精华内容
下载资源
问答
  • Hello大家,这一次睿老师将会带领大家实现mnist的手写数字识别,手把手教会你如何使用TensorFlow进行基本的操作。本次课程中,我会保持一贯的啰嗦风格,事无巨细地深入讲解每一个操作细节,为每一位读者提供最贴心...

    Hello大家好,这一次睿老师将会带领大家实现mnist的手写数字识别,手把手教会你如何使用TensorFlow进行基本的操作。本次课程中,我会保持一贯的啰嗦风格,事无巨细地深入讲解每一个操作细节,为每一位读者提供最贴心的服务。

    准备工作

    安装TensorFlow我就不多说了,主要是mnist数据集的获取一般都会有些问题。你可能会遇到input_data.py文件下载不下来,又或是下载input_data.py之后运行会报错,又或是使用tensorflow.examples.tutorials.mnist 里自带的input_data.py报错。
    解决方法如下:

    1、Input_data.py文件需要修改一下下才能正常使用,在37行的“return
    numpy.frombuffer(bytestream.read(4), dtype=dt)”:末尾处添加“[0]”,变成“return
    numpy.frombuffer(bytestream.read(4), dtype=dt)[0]”
    2、由于网络问题或其他一些未知原因,input_data下载、解压mnist数据集时会报错,这时你只要自己手动下载好数据集(四个压缩包)放到工作空间的MNIST_data文件夹里就行,就能绕开报错问题。

    流程讲解

    大致流程分为三步:
    1、构建CNN网络结构;
    2、构建loss function,配置寻优器;
    3、训练、测试。

    神经网络总体结构概览:

    本教程中使用了两个卷积层+池化层,最后接上两个全连接层。
    第一层卷积使用32个3x3x1的卷积核,步长为1,边界处理方式为“SAME”(卷积的输入和输出保持相同尺寸),激发函数为Relu,后接一个2x2的池化层,方式为最大化池化;
    第二层卷积使用50个3x3x32的卷积核,步长为1,边界处理方式为“SAME”,激发函数为Relu, 后接一个2x2的池化层,方式为最大化池化;
    第一层全连接层:使用1024个神经元,激发函数依然是Relu。
    第二层全连接层:使用10个神经元,激发函数为softmax,用于输出结果。

    代码概览:

    import input_data
    import tensorflow as tf
    #读取数据
    mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
    sess=tf.InteractiveSession()
    #构建cnn网络结构
    #自定义卷积函数(后面卷积时就不用写太多)
    def conv2d(x,w):
    return tf.nn.conv2d(x,w,strides=[1,1,1,1],padding='SAME') 
    #自定义池化函数 
    def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')
    #设置占位符,尺寸为样本输入和输出的尺寸
    x=tf.placeholder(tf.float32,[None,784])
    y_=tf.placeholder(tf.float32,[None,10])
    x_img=tf.reshape(x,[-1,28,28,1])
    
    #设置第一个卷积层和池化层
    w_conv1=tf.Variable(tf.truncated_normal([3,3,1,32],stddev=0.1))
    b_conv1=tf.Variable(tf.constant(0.1,shape=[32]))
    h_conv1=tf.nn.relu(conv2d(x_img,w_conv1)+b_conv1)
    h_pool1=max_pool_2x2(h_conv1)
    
    #设置第二个卷积层和池化层
    w_conv2=tf.Variable(tf.truncated_normal([3,3,32,50],stddev=0.1))
    b_conv2=tf.Variable(tf.constant(0.1,shape=[50]))
    h_conv2=tf.nn.relu(conv2d(h_pool1,w_conv2)+b_conv2)
    h_pool2=max_pool_2x2(h_conv2)
    
    #设置第一个全连接层
    w_fc1=tf.Variable(tf.truncated_normal([7*7*50,1024],stddev=0.1))
    b_fc1=tf.Variable(tf.constant(0.1,shape=[1024]))
    h_pool2_flat=tf.reshape(h_pool2,[-1,7*7*50])
    h_fc1=tf.nn.relu(tf.matmul(h_pool2_flat,w_fc1)+b_fc1)
    
    #dropout(随机权重失活)
    keep_prob=tf.placeholder(tf.float32)
    h_fc1_drop=tf.nn.dropout(h_fc1,keep_prob)
    
    #设置第二个全连接层
    w_fc2=tf.Variable(tf.truncated_normal([1024,10],stddev=0.1))
    b_fc2=tf.Variable(tf.constant(0.1,shape=[10]))
    y_out=tf.nn.softmax(tf.matmul(h_fc1_drop,w_fc2)+b_fc2)
    
    #建立loss function,为交叉熵
    loss=tf.reduce_mean(-tf.reduce_sum(y_*tf.log(y_out),reduction_indices=[1]))
    #配置Adam优化器,学习速率为1e-4
    train_step=tf.train.AdamOptimizer(1e-4).minimize(loss)
    
    #建立正确率计算表达式
    correct_prediction=tf.equal(tf.argmax(y_out,1),tf.argmax(y_,1))
    accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))
    
    #开始喂数据,训练
    tf.global_variables_initializer().run()
    for i in range(20000):
        batch=mnist.train.next_batch(50)
        if i%100==0:
            train_accuracy=accuracy.eval(feed_dict={x:batch[0],y_:batch[1],keep_prob:1})
            print "step %d,train_accuracy= %g"%(i,train_accuracy)
        train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5})
    
    #训练之后,使用测试集进行测试,输出最终结果
    print "test_accuracy= %g"%accuracy.eval(feed_dict={x:mnist.test.images,y_:mnist.test.labels,keep_prob:1})

    代码逐段解析:

    1.

    #自定义卷积函数(后面就不用写太多)
    def conv2d(x,w):
    return tf.nn.conv2d(x,w,strides=[1,1,1,1],padding='SAME') 
    #自定义池化函数 
    def max_pool_2x2(x):
    return tf.nn.max_pool(x,ksize=[1,2,2,1],strides=[1,2,2,1],padding='SAME')

    卷积步长为1,如要改成步长为2则strides=[1,2,2,1],只有中间两个是有效的(对于二维图来说),使用‘SAME’的padding方法(即输出与输入保持相同尺寸,边界处少一两个像素则自动补上);池化层的设置也类似,池化尺寸为2X2。

    2.

    #设置占位符,尺寸为样本输入和输出的尺寸
    x=tf.placeholder(tf.float32,[None,784])
    y_=tf.placeholder(tf.float32,[None,10])
    x_img=tf.reshape(x,[-1,28,28,1])

    设置输入输出的占位符,占位符是向一个会话中喂数据的入口,因为TensorFlow的使用中,通过构建计算图来设计网络,而网络的运行计算则在会话中启动,这个过程我们无法直接介入,需要通过placeholder来对一个会话进行数据输入。
    占位符设置好之后,将x变形成为28x28是矩阵形式(tf.reshape()函数)。

    3.

    #设置第一个卷积层和池化层
    w_conv1=tf.Variable(tf.truncated_normal([3,3,1,32],stddev=0.1))
    b_conv1=tf.Variable(tf.constant(0.1,shape=[32]))
    h_conv1=tf.nn.relu(conv2d(x_img,w_conv1)+b_conv1)
    h_pool1=max_pool_2x2(h_conv1)
    
    #设置第二个卷积层和池化层
    w_conv2=tf.Variable(tf.truncated_normal([3,3,32,50],stddev=0.1))
    b_conv2=tf.Variable(tf.constant(0.1,shape=[50]))
    h_conv2=tf.nn.relu(conv2d(h_pool1,w_conv2)+b_conv2)
    h_pool2=max_pool_2x2(h_conv2)

    第一层卷积使用3x3x1的卷积核,一共有32 个卷积核,权值使用方差为0.1的截断正态分布(指最大值不超过方差两倍的分布)来初始化,偏置的初值设定为常值0.1。
    第二层卷积和第一层类似,卷积核尺寸为3x3x32(32是通道数,因为上一层使用32个卷积核,所以这一层的通道数就变成了32),这一层一共使用50个卷积核,其他设置与上一层相同。
    每一层卷积完之后接上一个2x2的最大化池化操作。

    4.

    #设置第一个全连接层
    w_fc1=tf.Variable(tf.truncated_normal([7*7*50,1024],stddev=0.1))
    b_fc1=tf.Variable(tf.constant(0.1,shape=[1024]))
    h_pool2_flat=tf.reshape(h_pool2,[-1,7*7*50])
    h_fc1=tf.nn.relu(tf.matmul(h_pool2_flat,w_fc1)+b_fc1)
    
    #dropout(随机权重失活)
    keep_prob=tf.placeholder(tf.float32)
    h_fc1_drop=tf.nn.dropout(h_fc1,keep_prob)
    
    #设置第二个全连接层
    w_fc2=tf.Variable(tf.truncated_normal([1024,10],stddev=0.1))
    b_fc2=tf.Variable(tf.constant(0.1,shape=[10]))
    y_out=tf.nn.softmax(tf.matmul(h_fc1_drop,w_fc2)+b_fc2)

    卷积层之后就是两个全连接层,第一个全连接层有1024个神经元,先将卷积层得到的2x2输出展开成一长条,使用Relu激活函数得到输出,输出为1024维。
    Dropout:在这一层使用dropout(权值随机失活),对一些神经元突触连接进行强制的置零,这个trick可以防止神经网络过拟合。这里的dropout的保留比例是0.5,即随机地保留一半权值,删除另外一半(不要觉得可惜,为了保证在测试集上的效果,这是必须的)。Dropout比例通过placeholder来设置,因为训练过程中需要dropout,但是在最后的测试过程中,我们又希望使用全部的权值,所以dropout的比例要能够改变,所以这里使用placeholder。
    第二个全连接层有10个神经元,分别对应0-9这10个数字(没错,这一层终于要得到最终的识别结果了),不过与之前的每一层不同的是,这里使用的激活函数是Softmax,关于softmax,我的个人理解是softmax是“以指数函数作为核函数的归一化操作”,softmax与一般归一化操作不同的是,指数函数能够放大一个分布内各个数值的差异,能够使各个数值的“贫富差距”变大,“两极分化”现象会更明显(对同一个分布进行一般的归一化得到的分布和softmax得到的分布,softmax得到的分布信息熵要更大)

    5.

    #建立loss function,为交叉熵
    loss=tf.reduce_mean(-tf.reduce_sum(y_*tf.log(y_out),reduction_indices=[1]))
    #配置Adam优化器,学习速率为1e-4
    train_step=tf.train.AdamOptimizer(1e-4).minimize(loss)
    
    #建立正确率计算表达式
    correct_prediction=tf.equal(tf.argmax(y_out,1),tf.argmax(y_,1))
    accuracy=tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

    建立loss function是很重要的一个过程,这里使用交叉熵来作为loss,交叉熵是用来衡量两个分布的相似程度的,两个分布越接近,则交叉熵越小。
    使用Adam优化器来最小化loss,配置学习速率为1e-4。然后建立正确率的计算表达式(注意,仅仅是建立而已,现在还没有开始真正的计算),tf.argmax(y_,1),函数用来返回其中最大的值的下标,tf.equal()用来计算两个值是否相等。tf.cast()函数用来实现数据类型转换(这里是转换为float32),tf.reduce_mean()用来求平均(得到正确率)。

    6.

    #开始喂数据,训练
    tf.global_variables_initializer().run()
    for i in range(20000):
        batch=mnist.train.next_batch(50)
        if i%100==0:
            train_accuracy=accuracy.eval(feed_dict={x:batch[0],y_:batch[1],keep_prob:1})
            print "step %d,train_accuracy= %g"%(i,train_accuracy)
        train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5})#这里才开始真正进行训练计算

    接下来就是喂数据了,对网络进行训练了,首先使用tf.global_variables_initializer().run()初始化所有数据,从mnist训练数据集中一次取50个样本作为一组进行训练,一共进行20000组训练,每100次就输出一次该组数据上的正确率。
    进行训练计算的方式是:train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5}),通过feed_dict来对会话输送训练数据(以及其他一些想在计算过程中实时调整的参数,比如dropout比例)。
    这段代码中可以看到,训练时dropout的保留比例是0.5,测试时的保留比例是1。

    7.

    #训练之后,使用测试集进行测试,输出最终结果
    Print "test_accuracy= %g"%accuracy.eval(feed_dict={x:mnist.test.images,y_:mnist.test.labels,keep_prob:1})

    最后输入测试数据集进行测试验证,把代码跑起来看看结果吧。

    接下来就是喂数据了,对网络进行训练了,首先使用tf.global_variables_initializer().run()初始化所有数据,从mnist训练数据集中一次取50个样本作为一组进行训练,一共进行20000组训练,每100次就输出一次该组数据上的正确率。
    进行训练计算的方式是:train_step.run(feed_dict={x:batch[0],y_:batch[1],keep_prob:0.5}),通过feed_dict来对会话输送训练数据(以及其他一些想在计算过程中实时调整的参数,比如dropout比例)。
    这段代码中可以看到,训练时dropout的保留比例是0.5,测试时的保留比例是1。

    运行结果

    最后的运行结果长这样(测试集上的准确率为99.21%,还不错):

    Extracting MNIST_data/train-images-idx3-ubyte.gz
    Extracting MNIST_data/train-labels-idx1-ubyte.gz
    Extracting MNIST_data/t10k-images-idx3-ubyte.gz
    Extracting MNIST_data/t10k-labels-idx1-ubyte.gz
    step 0,train_accuracy= 0.1
    step 100,train_accuracy= 0.76
    step 200,train_accuracy= 0.9
    step 300,train_accuracy= 0.84
    step 400,train_accuracy= 0.98
    ……
    ……
    ……
    step 19600,train_accuracy= 1
    step 19700,train_accuracy= 0.98
    step 19800,train_accuracy= 1
    step 19900,train_accuracy= 1
    test_accuracy= 0.9921

    参考资料:
    [1]. 《TensorFlow实战》 黄文坚

    展开全文
  • 给 python 程序员的 OpenGL 教程

    万次阅读 多人点赞 2019-01-26 08:47:24
    OpenGL 不是一个独立的平台,因此,它需要借助于一种编程语言才能被使用。C / C++ / python / java 都可以很支持 OpengGL,我当然习惯性选择 python 语言。 如果读者是 python 程序员,并且了解 numpy,接下来...

    1 预备知识

    OpenGL 是 Open Graphics Library 的简写,意为“开放式图形库”,是用于渲染 2D、3D 矢量图形的跨语言、跨平台的应用程序编程接口(API)。OpenGL 不是一个独立的平台,因此,它需要借助于一种编程语言才能被使用。C / C++ / python / java 都可以很好支持 OpengGL,我当然习惯性选择 python 语言。

    如果读者是 python 程序员,并且了解 numpy,接下来的阅读应该不会有任何障碍;否则,我建议先花半小时学习一下 python 语言。关于 numpy,可以参考我的另一篇博文《数学建模三剑客MSN》。事实上,我觉得 python 语言近乎于自然语言,只要读者是程序员,即便不熟悉 python,读起来也不会有多大问题。

    另外,读者也不必担心数学问题。使用 OpenGL 不需要具备多么高深的数学水平,只要能辅导初中学生的数学作业,就足够用了。

    1.1 坐标系

    在 OpenGL 的世界里,有各式各样的坐标系。随着对 OpenGL 概念的理解,我们至少会接触到六种坐标系,而初始只需要了解其中的三个就足够用了(第一次阅读这段话的时候,只需要了解世界坐标系就可以了)。

    • 世界坐标系(World Coordinates)

      世界坐标系是右手坐标系,以屏幕中心为原点(0, 0, 0),且是始终不变的。 在这里插入图片描述

    • 视点坐标系(Eye or Camera Coordinates)

      视点坐标是以视点为原点,以视线的方向为Z+轴正方向的坐标系。OpenGL 管道会将世界坐标先变换到视点坐标,然后进行裁剪,只有在视线范围(视景体)之内的场景才会进入下一阶段的计算。

    • 屏幕坐标系(Window or Screen Coordinates)

      OpenGL 的重要功能之一就是将三维的世界坐标经过变换、投影等计算,最终算出它在显示设备上对应的位置,这个位置就称为设备坐标。在屏幕、打印机等设备上的坐标是二维坐标。值得一提的是,OpenGL 可以只使用设备的一部分进行绘制,这个部分称为视区或视口(viewport)。投影得到的是视区内的坐标(投影坐标),从投影坐标到设备坐标的计算过程就是设备变换了。

    1.2 投影

    三维场景中的物体最终都会显示在类似屏幕这样的二维观察平面上。将三维物体变为二维图形的变换成为投影变换。最常用的投影有两种:平行投影和透视投影。如下图所示,F 是投影面,p1p2 为三维空间中的一条直线,p’1 和 p’2 分别是 p1 和 p2 在 F 上的投影,虚线表示投影线,O 为投影中心。
    在这里插入图片描述

    • 平行投影
      这里所说的平行投影,特指正交平行投影——投影线垂直于投影面。将一个三维点 (x,y,z) 正交平行投影到 xoy 平面上,则投影点坐标为 (x,y,0)。由于平行投影丢弃了深度信息,所以无法产生真实感,但可以保持物体之间相对大小关系不变。

    • 透视投影
      透视投影将投影面置于观察点和投影对象之间,距离观察者越远的物体,投影尺寸越小,投影效果具有真实感,常用于游戏和仿真领域。

    1.3 视景体

    无论是平行投影还是透视投影,投影成像都是在投影面上——我们可以把投影面理解成显示屏幕。世界坐标系描述的三维空间是无限的,投影平面是无限的,但(我们能够看到的)屏幕面积总是有限的,因此在投影变换时,通常只处理能够显示在屏幕上的那一部分三维空间。从无限三维空间中裁切出来的可以显示在屏幕上的部分三维空间,我们称之为视景体。视景体有六个面,分别是左右上下和前后面。

    对于平行投影而言,视景体是一个矩形平行六面体;对于透视投影来说,视景体是一个棱台。理解这一点并不难:因为越远处的物体在投影窗口的透视投影越小,也就意味着填满投影窗口需要更大的体量,视景体自然就变成了棱台。
    在这里插入图片描述

    1.4 视口

    对于平行投影而言,视口就是由视景体的左右上下四个面围成的矩形,对于透视投影来说,视口就是视景体的前截面在投影窗口上的透视投影。

    视口是 OpenGL 中比较重要的概念,现阶段可以简单理解成屏幕(或其他输出设备)。事实上,视口和屏幕是相关但又不相同的,屏幕有固定的宽高比,而视口大小可以由用户自行定义。通常,为了适应不同宽高比的屏幕,在设置视口时,会根据屏幕宽高比调整视景体(增加宽度或高度)。

    1.5 视点

    现实生活中,人们看到的三维空间物体的样子取决于观察者站在什么角度去看。这里面包含着三个概念:

    • 观察者的位置:眼睛在哪儿?
    • 观察者的姿势:站立还是倒立?左侧卧还是右侧卧?
    • 观察对象:眼睛盯着哪里?

    对应在 OpenGL 中,也有同样的概念,即视点的位置、瞄准方向的参考点,以及(向上的)方向。

    1.6 OpenGL 变换

    下图是三维图形的显示流程。世界坐标系中的三维物体经过视点变换和一系列几何变换(平移、旋转、缩放)之后,坐标系变换为视点坐标系;经过投影和裁剪之后,坐标系变换为归一化设备坐标系;最后经过视口变换显示在屏幕上,相应地,坐标系变成了窗口坐标系。
    在这里插入图片描述

    • 视点变换:相当于设置视点的位置和方向
    • 模型变换:包括平移、旋转、缩放等三种类型
    • 裁剪变换:根据视景体定义的六个面(和附加裁剪面)对三维空间裁剪
    • 视口变换:将视景体内投影的物体显示在二维的视口平面上

    2 安装 pyopengl

    如果想当然地使用 pip 如下所示安装,可能会有一些麻烦。

    pip install pyopengl
    

    当我这样安装之后,运行 OpenGL 代码,得到了这样的错误信息:

    NullFunctionError: Attempt to call an undefined function glutInit, check for bool(glutInit) before calling
    

    原来,pip 默认安装的是32位版本的pyopengl,而我的操作系统是64位的。建议点击这里下载适合自己的版本,直接安装.whl文件。我是这样安装的:

    pip install PyOpenGL-3.1.3b2-cp37-cp37m-win_amd64.whl
    

    3 OpenGL 库及函数简介

    我第一次接触 OpenGL 的 GL / GLU / GLUT 的时候,一下就被这些长得像孪生兄弟的库名字给整懵圈了,要不是内心强大,也许就跟 OpenGL 说再见了。时间久了才发现,OpenGL 的库及函数命名规则非常合理,便于查找、记忆。

    OpenGL函数的命名格式如下:

    <库前缀><根命令><可选的参数个数><可选的参数类型>
    

    常见的库前缀有 gl、glu、glut、aux、wgl、glx、agl 等。库前缀表示该函数属于 OpenGL 哪一个开发库。从函数名后面中还可以看出需要多少个参数以及参数的类型。I 代表 int 型,f 代表 float 型,d 代表 double 型,u 代表无符号整型。例如 glColor3f() 表示了该函数属于gl库,参数是三个浮点数。

    OpenGL 函数库相关的 API 有核心库(gl)、实用库(glu)、实用工具库(glut)、辅助库(aux)、窗口库(glx、agl、wgl)和扩展函数库等。gl是核心,glu是对gl的部分封装。glut是为跨平台的OpenGL程序的工具包,比aux功能强大。glx、agl、wgl 是针对不同窗口系统的函数。扩展函数库是硬件厂商为实现硬件更新利用OpenGL的扩展机制开发的函数。本文仅对常用的四个库做简单介绍。

    3.1 OpenGL 核心库 GL

    核心库包含有115个函数,函数名的前缀为gl。这部分函数用于常规的、核心的图形处理。此函数由gl.dll来负责解释执行。由于许多函数可以接收不同数以下几类。据类型的参数,因此派生出来的函数原形多达300多个。核心库中的函数主要可以分为以下几类函数:

    • 绘制基本几何图元的函数:
      glBegain()、glEnd()、glNormal*()、glVertex*()

    • 矩阵操作、几何变换和投影变换的函数:
      如矩阵入栈函数glPushMatrix(),矩阵出栈函数glPopMatrix(),装载矩阵函数glLoadMatrix(),矩阵相乘函数glMultMatrix(),当前矩阵函数glMatrixMode()和矩阵标准化函数glLoadIdentity(),几何变换函数glTranslate*()、glRotate*()和glScale*(),投影变换函数glOrtho()、glFrustum()和视口变换函数glViewport()

    • 颜色、光照和材质的函数:
      如设置颜色模式函数glColor*()、glIndex*(),设置光照效果的函数glLight*() 、glLightModel*()和设置材质效果函数glMaterial()

    • 显示列表函数:
      主要有创建、结束、生成、删除和调用显示列表的函数glNewList()、glEndList()、glGenLists()、glCallList()和glDeleteLists()

    • 纹理映射函数:
      主要有一维纹理函数glTexImage1D()、二维纹理函数glTexImage2D()、设置纹理参数、纹理环境和纹理坐标的函数glTexParameter*()、glTexEnv*()和glTetCoord*()

    • 特殊效果函数:
      融合函数glBlendFunc()、反走样函数glHint()和雾化效果glFog*()

    • 光栅化、象素操作函数:
      如象素位置glRasterPos*()、线型宽度glLineWidth()、多边形绘制模式glPolygonMode(),读取象素glReadPixel()、复制象素glCopyPixel()

    • 选择与反馈函数:
      主要有渲染模式glRenderMode()、选择缓冲区glSelectBuffer()和反馈缓冲区glFeedbackBuffer()

    • 曲线与曲面的绘制函数:
      生成曲线或曲面的函数glMap*()、glMapGrid*(),求值器的函数glEvalCoord*() glEvalMesh*()

    • 状态设置与查询函数:
      glGet*()、glEnable()、glGetError()

    3.2 OpenGL 实用库 GLU

    包含有43个函数,函数名的前缀为glu。OpenGL提供了强大的但是为数不多的绘图命令,所有较复杂的绘图都必须从点、线、面开始。Glu 为了减轻繁重的编程工作,封装了OpenGL函数,Glu函数通过调用核心库的函数,为开发者提供相对简单的用法,实现一些较为复杂的操作。此函数由glu.dll来负责解释执行。OpenGL中的核心库和实用库可以在所有的OpenGL平台上运行。主要包括了以下几种:

    • 辅助纹理贴图函数:
      gluScaleImage() 、gluBuild1Dmipmaps()、gluBuild2Dmipmaps()

    • 坐标转换和投影变换函数:
      定义投影方式函数gluPerspective()、gluOrtho2D() 、gluLookAt(),拾取投影视景体函数gluPickMatrix(),投影矩阵计算gluProject()和gluUnProject()

    • 多边形镶嵌工具:
      gluNewTess()、gluDeleteTess()、gluTessCallback()、gluBeginPolygon()、gluTessVertex()、gluNextContour()、gluEndPolygon()

    • 二次曲面绘制工具:
      主要有绘制球面、锥面、柱面、圆环面gluNewQuadric()、gluSphere()、gluCylinder()、gluDisk()、gluPartialDisk()、gluDeleteQuadric()

    • 非均匀有理B样条绘制工具:
      主要用来定义和绘制Nurbs曲线和曲面,包括gluNewNurbsRenderer()、gluNurbsCurve()、gluBeginSurface()、gluEndSurface()、gluBeginCurve()、gluNurbsProperty()

    • 错误反馈工具:
      获取出错信息的字符串gluErrorString()

    3.3 OpenGL 工具库 GLUT

    包含大约30多个函数,函数名前缀为glut。glut是不依赖于窗口平台的OpenGL工具包,由Mark KLilgrad在SGI编写(现在在Nvidia),目的是隐藏不同窗口平台API的复杂度。函数以glut开头,它们作为aux库功能更强的替代品,提供更为复杂的绘制功能,此函数由glut.dll来负责解释执行。由于glut中的窗口管理函数是不依赖于运行环境的,因此OpenGL中的工具库可以在X-Window, Windows NT, OS/2等系统下运行,特别适合于开发不需要复杂界面的OpenGL示例程序。对于有经验的程序员来说,一般先用glut理顺3D图形代码,然后再集成为完整的应用程序。这部分函数主要包括:

    • 窗口操作函数:
      窗口初始化、窗口大小、窗口位置函数等 glutInit()、glutInitDisplayMode()、glutInitWindowSize()、glutInitWindowPosition()

    • 回调函数:
      响应刷新消息、键盘消息、鼠标消息、定时器函数 GlutDisplayFunc()、glutPostRedisplay()、glutReshapeFunc()、glutTimerFunc()、glutKeyboardFunc()、glutMouseFunc()

    • 创建复杂的三维物体:
      这些和aux库的函数功能相同

    • 菜单函数:
      创建添加菜单的函数 GlutCreateMenu()、glutSetMenu()、glutAddMenuEntry()、glutAddSubMenu() 和 glutAttachMenu()

    • 程序运行函数:
      glutMainLoop()

    3.4 Windows 专用库 WGL

    针对windows平台的扩展,包含有16个函数,函数名前缀为wgl。这部分函数主要用于连接OpenGL和Windows ,以弥补OpenGL在文本方面的不足。 Windows专用库只能用于Windows环境中。这类函数主要包括以下几类:

    • 绘图上下文相关函数:
      wglCreateContext()、wglDeleteContext()、wglGetCurrentContent()、wglGetCurrentDC()、wglDeleteContent()

    • 文字和文本处理函数:
      wglUseFontBitmaps()、wglUseFontOutlines()

    • 覆盖层、地层和主平面层处理函数:
      wglCopyContext()、wglCreateLayerPlane()、wglDescribeLayerPlane()、wglReakizeLayerPlatte()

    • 其他函数:
      wglShareLists()、wglGetProcAddress()

    4 开始 OpenGL 的奇幻之旅

    4.1 OpenGL 基本图形的绘制

    4.1.1 设置颜色

    设置颜色的函数有几十个,都是以 glColor 开头,后面跟着参数个数和参数类型。参数可以是 0 到 255 之间的无符号整数,也可以是 0 到 1 之间的浮点数。三个参数分别表示 RGB 分量,第四个参数表示透明度(其实叫不透明度更恰当)。以下最常用的两个设置颜色的方法:

    glColor3f(1.00.00.0)  # 设置当前颜色为红色
    glColor4f(0.01.01.01.0)  # 设置当前颜色为青色,不透明度
    glColor3ub(0, 0, 255)  # 设置当前颜色为蓝色
    

    glColor 也支持将三个或四个参数以向量方式传递,例如:

    glColor3fv([0.01.00.0])  # 设置当前颜色为绿色
    

    特别提示:OpenGL 是使用状态机模式,颜色是一个状态变量,设置颜色就是改变这个状态变量并一直生效,直到再次调用设置颜色的函数。除了颜色,OpenGL 还有很多的状态变量或模式。在任何时间,都可以查询每个状态变量的当前值,还可以用 glPushAttrib() 或 glPushClientAttrib() 把状态变量的集合保存起来,必要的时候,再用 glPopAttrib() 或 glPopClientAttrib() 恢复状态变量。

    4.1.2 设置顶点

    顶点(vertex)是 OpengGL 中非常重要的概念,描述线段、多边形都离不开顶点。和设置颜色类似,设置顶点的函数也有几十个,都是以 glVertex 开头,后面跟着参数个数和参数类型,同样也支持将多个以向量方式传递。 两个参数的话,分别表示 xy 坐标,三个参数则分别表示 xyz 坐标。如有第四个参数,则表示该点的齐次坐标 w;否则,默认 w=1。至于什么是齐次坐标,显然超出了初中数学的范畴,在此不做探讨。

    glVertex2f(1.00.5) # xoy平面上的点,z=0
    glVertex3f(0.51.00.0) # 三维空间中的点
    

    4.1.3 绘制基本图形

    仅仅设置颜色和顶点,并不能画出来什么。我们可以在任何时候改变颜色,但所有的顶点设置,都必须包含在 glBegin() 和 glEnd() 之间,而 glBegin() 的参数则指定了将这些顶点画成什么。以下是 glBegin() 可能的参数选项:

    参数 说明
    GL_POINTS 绘制一个或多个顶点
    GL_LINES 绘制线段
    GL_LINE_STRIP 绘制连续线段
    GL_LINE_LOOP 绘制闭合的线段
    GL_POLYGON 绘制多边形
    GL_TRIANGLES 绘制一个或多个三角形
    GL_TRIANGLE_STRIP 绘制连续三角形
    GL_TRIANGLE_FAN 绘制多个三角形组成的扇形
    GL_QUADS 绘制一个或多个四边形
    GL_QUAD_STRIP 绘制连续四边形

    4.2 第一个 OpenGL 程序

    通常,我们使用工具库(GLUT)创建 OpenGL 应用程序。为啥不用 GL 或者 GLU 库呢?画画之前总得先有一块画布吧,不能直接拿起画笔就开画。前文说过,工具库主要提供窗口相关的函数,有了窗口,就相当于有了画布,而核心库和实用库,就好比各式各样的画笔、颜料。使用工具库(GLUT)创建 OpenGL 应用程序只需要四步(当然,前提是你需要先准备好绘图函数,并给它取一个合适的名字):

    1. 初始化glut库
    2. 创建glut窗口
    3. 注册绘图的回调函数
    4. 进入glut主循环

    OK,铺垫了这么多之后,我们终于开始第一个 OpenGL 应用程序了:绘制三维空间的世界坐标系,在坐标原点的后方(z轴的负半区)画一个三角形。代码如下:

    # -*- coding: utf-8 -*-
    
    # -------------------------------------------
    # quidam_01.py 三维空间的世界坐标系和三角形
    # -------------------------------------------
    
    from OpenGL.GL import *
    from OpenGL.GLUT import *
    
    def draw():
        # ---------------------------------------------------------------
        glBegin(GL_LINES)                    # 开始绘制线段(世界坐标系)
        
        # 以红色绘制x轴
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.8, 0.0, 0.0)           # 设置x轴顶点(x轴负方向)
        glVertex3f(0.8, 0.0, 0.0)            # 设置x轴顶点(x轴正方向)
        
        # 以绿色绘制y轴
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.0, -0.8, 0.0)           # 设置y轴顶点(y轴负方向)
        glVertex3f(0.0, 0.8, 0.0)            # 设置y轴顶点(y轴正方向)
        
        # 以蓝色绘制z轴
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, 0.0, -0.8)           # 设置z轴顶点(z轴负方向)
        glVertex3f(0.0, 0.0, 0.8)            # 设置z轴顶点(z轴正方向)
        
        glEnd()                              # 结束绘制线段
        
        # ---------------------------------------------------------------
        glBegin(GL_TRIANGLES)                # 开始绘制三角形(z轴负半区)
        
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.5, -0.366, -0.5)       # 设置三角形顶点
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.5, -0.366, -0.5)        # 设置三角形顶点
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, 0.5, -0.5)           # 设置三角形顶点
        
        glEnd()                              # 结束绘制三角形
        
        # ---------------------------------------------------------------
        glFlush()                            # 清空缓冲区,将指令送往硬件立即执行
    
    if __name__ == "__main__":
        glutInit()                           # 1. 初始化glut库
        glutCreateWindow('Quidam Of OpenGL') # 2. 创建glut窗口
        glutDisplayFunc(draw)                # 3. 注册回调函数draw()
        glutMainLoop()                       # 4. 进入glut主循环
    

    运行代码,我这里显示结果如下面左图所示。如果尝试运行这段代码出错的话,我猜应该是 pyopengl 安装出现了问题,建议返回到前面重读 pyopengl 的安装。
    在这里插入图片描述
    短暂的激动之后,你可能会尝试画一些其他的线段,变换颜色或者透明度,甚至绘制多边形。很快你会发现,我们的第一个程序有很多问题,比如:

    1. 窗口的标题不能使用中文,否则会显示乱码
    2. 窗口的初始大小和位置无法改变
    3. 改变窗口的宽高比,三角形宽高比也会改变(如上面右图所示)
    4. 三角形不应该遮挡坐标轴
    5. 改变颜色的透明度无效
    6. 不能缩放旋转

    没关系,除了第1个问题我不知道怎么解决(貌似无解),其他问题都不是事儿。和我们的代码相比,一个真正实用的 OpenGL 程序,还有许多工作要做:

    • 设置初始显示模式
    • 初始化画布
    • 绘图函数里面需要增加:
      1. 清除屏幕及深度缓存
      2. 投影设置
      3. 模型试图设置
    • 绑定鼠标键盘的事件函数

    4.3 设置初始显示模式

    初始化 glut 库的时候,我们一般都要用 glutInitDisplayMode() 来设置初始的显示模式,它的参数可以是下表中参数的组合。

    参数 说明
    GLUT_RGB 指定RGB颜色模式的窗口
    GLUT_RGBA 指定RGBA 颜色模式的窗口
    GLUT_INDEX 指定颜色索引模式的窗口
    GLUT_SINGLE 指定单缓存窗口
    GLUT_DOUBLE 指定双缓存窗口
    GLUT_ACCUM 窗口使用累加缓存
    GLUT_ALPHA 窗口的颜色分量包含 alpha 值
    GLUT_DEPTH 窗口使用深度缓存
    GLUT_STENCIL 窗口使用模板缓存
    GLUT_MULTISAMPLE 指定支持多样本功能的窗口
    GLUT_STEREO 指定立体窗口
    GLUT_LUMINANCE 窗口使用亮度颜色模型

    使用双缓存窗口,可以避免重绘时产生抖动的感觉。我一般选择 GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH 作为参数来设置初始的显示模式。

    4.4 初始化画布

    开始绘图之前,需要对画布做一些初始化工作,这些工作只需要做一次。比如:

    glClearColor(0.0, 0.0, 0.0, 1.0) # 设置画布背景色。注意:这里必须是4个参数
    glEnable(GL_DEPTH_TEST)          # 开启深度测试,实现遮挡关系
    glDepthFunc(GL_LEQUAL)           # 设置深度测试函数(GL_LEQUAL只是选项之一)
    

    如有必要,还可以开启失真校正(反走样)、开启表面剔除等。

    4.5 清除屏幕及深度缓存

    每次重绘之前,需要先清除屏幕及深度缓存。这项操作一般放在绘图函数的开头。

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 
    

    4.5 设置投影

    投影设置也是每次重绘都需要的步骤之一。glOrtho() 用来设置平行投影,glFrustum() 用来设置透视投影。这两个函数的参数相同,都是视景体的 left / right / bottom / top / near / far 六个面。

    视景体的 left / right / bottom / top 四个面围成的矩形,就是视口。near 就是投影面,其值是投影面距离视点的距离,far 是视景体的后截面,其值是后截面距离视点的距离。far 和 near 的差值,就是视景体的深度。视点和视景体的相对位置关系是固定的,视点移动时,视景体也随之移动。

    我个人认为,视景体是 OpengGL 最重要、最核心的概念,它和视口、视点、投影面、缩放、漫游等概念密切关联。只有正确理解了视景体,才能正确设置它的六个参数,才能呈现出我们期望的效果。

    为了在窗口宽高比改变时,绘制的对象仍然保持固定的宽高比,一般在做投影变换时,需要根据窗口的宽高比适当调整视景体的 left / right 或者 bottom / top 参数。

    假设 view 是视景体,width 和 height 是窗口的宽度和高度,在投影变换之前,需要先声明是对投影矩阵的操作,并将投影矩阵单位化:

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    if width > height:
    	k = width / height
    	glFrustum(view [0]*k, view [1]*k, view [2], view [3], view [4], view [5])
    else:
    	k = height / width
    	glFrustum(view [0], view [1], view [2]*k, view [3]*k, view [4], view [5])
    

    4.6 设置视点

    视点是和视景体关联的概念。设置视点需要考虑眼睛在哪儿、看哪儿、头顶朝哪儿,分别对应着eye, lookat 和 eye_up 三个向量。

    gluLookAt(
           eye[0], eye[1], eye[2], 
           look_at[0], look_at[1], look_at[2],
           eye_up[0], eye_up[1], eye_up[2]
       )
    

    4.7 设置视口

    视口也是和视景体关联的概念,相对简单一点。

    glViewport(0, 0, width, height)
    

    4.8 设置模型视图

    模型平移、旋转、缩放等几何变换,需要切换到模型矩阵:

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    glScale(1.0, 1.0, 1.0)
    

    4.9 捕捉鼠标事件、键盘事件和窗口事件

    GLUT 库提供了几个函数帮我们捕捉鼠标事件、键盘事件和窗口事件:

    1. glutMouseFunc()
      该函数捕捉鼠标点击和滚轮操作,返回4个参数给被绑定的事件函数:键(左键/右键/中键/滚轮上/滚轮下)、状态(1/0)、x坐标、y坐标

    2. glutMotionFunc()
      该函数捕捉有一个鼠标键被按下时的鼠标移动给被绑定的事件函数,返回2个参数:x坐标、y坐标

    3. glutPassiveMotionFunc()
      该函数捕捉鼠标移动,返回2个参数给被绑定的事件函数:x坐标、y坐标

    4. glutEntryFunc()
      该函数捕捉鼠标离开或进入窗口区域,返回1个参数给被绑定的事件函数:GLUT_LEFT 或者 GLUT_ENTERED

    5. glutKeyboardFunc(keydown)
      该函数捕捉键盘按键被按下,返回3个参数给被绑定的事件函数:被按下的键,x坐标、y坐标

    6. glutReshapeFunc()
      该函数捕捉窗口被改变大小,返回2个参数给被绑定的事件函数:窗口宽度、窗口高度

    如果我们需要捕捉这些事件,只需要定义事件函数,注册相应的函数就行:

    def reshape(width, height):
    	pass
    
    def mouseclick(button, state, x, y):
    	pass
    
    def mousemotion(x, y):
    	pass
    
    def keydown(key, x, y):
    	pass
    
    glutReshapeFunc(reshape)            # 注册响应窗口改变的函数reshape()
    glutMouseFunc(mouseclick)           # 注册响应鼠标点击的函数mouseclick()
    glutMotionFunc(mousemotion)         # 注册响应鼠标拖拽的函数mousemotion()
    glutKeyboardFunc(keydown)           # 注册键盘输入的函数keydown()
    

    4.10 综合应用

    是时候把我们上面讲的这些东西完整的演示一下了。下面的代码还是画了世界坐标系,并在原点前后各画了一个三角形。鼠标可以拖拽视点绕参考点旋转(二者距离保持不变),滚轮可以缩放模型。敲击退格键或回车键可以让视点远离或接近参考点。敲击 x/y/z 可以减小参考点对应的坐标值,敲击 X/Y/Z 可以增大参考点对应的坐标值。敲击空格键可以切换投影模式。
    在这里插入图片描述
    上图左是平行投影模式的显示效果,上图右是透视投影模式的显示效果。代码如下:

    # -*- coding: utf-8 -*-
    
    # -------------------------------------------
    # quidam_02.py 旋转、缩放、改变视点和参考点
    # -------------------------------------------
    
    from OpenGL.GL import *
    from OpenGL.GLU import *
    from OpenGL.GLUT import *
    import numpy as np
    
    IS_PERSPECTIVE = True                               # 透视投影
    VIEW = np.array([-0.8, 0.8, -0.8, 0.8, 1.0, 20.0])  # 视景体的left/right/bottom/top/near/far六个面
    SCALE_K = np.array([1.0, 1.0, 1.0])                 # 模型缩放比例
    EYE = np.array([0.0, 0.0, 2.0])                     # 眼睛的位置(默认z轴的正方向)
    LOOK_AT = np.array([0.0, 0.0, 0.0])                 # 瞄准方向的参考点(默认在坐标原点)
    EYE_UP = np.array([0.0, 1.0, 0.0])                  # 定义对观察者而言的上方(默认y轴的正方向)
    WIN_W, WIN_H = 640, 480                             # 保存窗口宽度和高度的变量
    LEFT_IS_DOWNED = False                              # 鼠标左键被按下
    MOUSE_X, MOUSE_Y = 0, 0                             # 考察鼠标位移量时保存的起始位置
    
    def getposture():
        global EYE, LOOK_AT
        
        dist = np.sqrt(np.power((EYE-LOOK_AT), 2).sum())
        if dist > 0:
            phi = np.arcsin((EYE[1]-LOOK_AT[1])/dist)
            theta = np.arcsin((EYE[0]-LOOK_AT[0])/(dist*np.cos(phi)))
        else:
            phi = 0.0
            theta = 0.0
            
        return dist, phi, theta
        
    DIST, PHI, THETA = getposture()                     # 眼睛与观察目标之间的距离、仰角、方位角
    
    def init():
        glClearColor(0.0, 0.0, 0.0, 1.0) # 设置画布背景色。注意:这里必须是4个参数
        glEnable(GL_DEPTH_TEST)          # 开启深度测试,实现遮挡关系
        glDepthFunc(GL_LEQUAL)           # 设置深度测试函数(GL_LEQUAL只是选项之一)
    
    def draw():
        global IS_PERSPECTIVE, VIEW
        global EYE, LOOK_AT, EYE_UP
        global SCALE_K
        global WIN_W, WIN_H
            
        # 清除屏幕及深度缓存
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        
        # 设置投影(透视投影)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        
        if WIN_W > WIN_H:
            if IS_PERSPECTIVE:
                glFrustum(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])
            else:
                glOrtho(VIEW[0]*WIN_W/WIN_H, VIEW[1]*WIN_W/WIN_H, VIEW[2], VIEW[3], VIEW[4], VIEW[5])
        else:
            if IS_PERSPECTIVE:
                glFrustum(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])
            else:
                glOrtho(VIEW[0], VIEW[1], VIEW[2]*WIN_H/WIN_W, VIEW[3]*WIN_H/WIN_W, VIEW[4], VIEW[5])
            
        # 设置模型视图
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
            
        # 几何变换
        glScale(SCALE_K[0], SCALE_K[1], SCALE_K[2])
            
        # 设置视点
        gluLookAt(
            EYE[0], EYE[1], EYE[2], 
            LOOK_AT[0], LOOK_AT[1], LOOK_AT[2],
            EYE_UP[0], EYE_UP[1], EYE_UP[2]
        )
        
        # 设置视口
        glViewport(0, 0, WIN_W, WIN_H)
        
        # ---------------------------------------------------------------
        glBegin(GL_LINES)                    # 开始绘制线段(世界坐标系)
        
        # 以红色绘制x轴
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.8, 0.0, 0.0)           # 设置x轴顶点(x轴负方向)
        glVertex3f(0.8, 0.0, 0.0)            # 设置x轴顶点(x轴正方向)
        
        # 以绿色绘制y轴
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.0, -0.8, 0.0)           # 设置y轴顶点(y轴负方向)
        glVertex3f(0.0, 0.8, 0.0)            # 设置y轴顶点(y轴正方向)
        
        # 以蓝色绘制z轴
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, 0.0, -0.8)           # 设置z轴顶点(z轴负方向)
        glVertex3f(0.0, 0.0, 0.8)            # 设置z轴顶点(z轴正方向)
        
        glEnd()                              # 结束绘制线段
        
        # ---------------------------------------------------------------
        glBegin(GL_TRIANGLES)                # 开始绘制三角形(z轴负半区)
        
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.5, -0.366, -0.5)       # 设置三角形顶点
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.5, -0.366, -0.5)        # 设置三角形顶点
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, 0.5, -0.5)           # 设置三角形顶点
        
        glEnd()                              # 结束绘制三角形
        
        # ---------------------------------------------------------------
        glBegin(GL_TRIANGLES)                # 开始绘制三角形(z轴正半区)
        
        glColor4f(1.0, 0.0, 0.0, 1.0)        # 设置当前颜色为红色不透明
        glVertex3f(-0.5, 0.5, 0.5)           # 设置三角形顶点
        glColor4f(0.0, 1.0, 0.0, 1.0)        # 设置当前颜色为绿色不透明
        glVertex3f(0.5, 0.5, 0.5)            # 设置三角形顶点
        glColor4f(0.0, 0.0, 1.0, 1.0)        # 设置当前颜色为蓝色不透明
        glVertex3f(0.0, -0.366, 0.5)         # 设置三角形顶点
        
        glEnd()                              # 结束绘制三角形
        
        # ---------------------------------------------------------------
        glutSwapBuffers()                    # 切换缓冲区,以显示绘制内容
        
    def reshape(width, height):
        global WIN_W, WIN_H
        
        WIN_W, WIN_H = width, height
        glutPostRedisplay()
        
    def mouseclick(button, state, x, y):
        global SCALE_K
        global LEFT_IS_DOWNED
        global MOUSE_X, MOUSE_Y
        
        MOUSE_X, MOUSE_Y = x, y
        if button == GLUT_LEFT_BUTTON:
            LEFT_IS_DOWNED = state==GLUT_DOWN
        elif button == 3:
            SCALE_K *= 1.05
            glutPostRedisplay()
        elif button == 4:
            SCALE_K *= 0.95
            glutPostRedisplay()
        
    def mousemotion(x, y):
        global LEFT_IS_DOWNED
        global EYE, EYE_UP
        global MOUSE_X, MOUSE_Y
        global DIST, PHI, THETA
        global WIN_W, WIN_H
        
        if LEFT_IS_DOWNED:
            dx = MOUSE_X - x
            dy = y - MOUSE_Y
            MOUSE_X, MOUSE_Y = x, y
            
            PHI += 2*np.pi*dy/WIN_H
            PHI %= 2*np.pi
            THETA += 2*np.pi*dx/WIN_W
            THETA %= 2*np.pi
            r = DIST*np.cos(PHI)
            
            EYE[1] = DIST*np.sin(PHI)
            EYE[0] = r*np.sin(THETA)
            EYE[2] = r*np.cos(THETA)
                
            if 0.5*np.pi < PHI < 1.5*np.pi:
                EYE_UP[1] = -1.0
            else:
                EYE_UP[1] = 1.0
            
            glutPostRedisplay()
        
    def keydown(key, x, y):
        global DIST, PHI, THETA
        global EYE, LOOK_AT, EYE_UP
        global IS_PERSPECTIVE, VIEW
        
        if key in [b'x', b'X', b'y', b'Y', b'z', b'Z']:
            if key == b'x': # 瞄准参考点 x 减小
                LOOK_AT[0] -= 0.01
            elif key == b'X': # 瞄准参考 x 增大
                LOOK_AT[0] += 0.01
            elif key == b'y': # 瞄准参考点 y 减小
                LOOK_AT[1] -= 0.01
            elif key == b'Y': # 瞄准参考点 y 增大
                LOOK_AT[1] += 0.01
            elif key == b'z': # 瞄准参考点 z 减小
                LOOK_AT[2] -= 0.01
            elif key == b'Z': # 瞄准参考点 z 增大
                LOOK_AT[2] += 0.01
            
            DIST, PHI, THETA = getposture()
            glutPostRedisplay()
        elif key == b'\r': # 回车键,视点前进
            EYE = LOOK_AT + (EYE - LOOK_AT) * 0.9
            DIST, PHI, THETA = getposture()
            glutPostRedisplay()
        elif key == b'\x08': # 退格键,视点后退
            EYE = LOOK_AT + (EYE - LOOK_AT) * 1.1
            DIST, PHI, THETA = getposture()
            glutPostRedisplay()
        elif key == b' ': # 空格键,切换投影模式
            IS_PERSPECTIVE = not IS_PERSPECTIVE 
            glutPostRedisplay()
    
    if __name__ == "__main__":
        glutInit()
        displayMode = GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH
        glutInitDisplayMode(displayMode)
    
        glutInitWindowSize(WIN_W, WIN_H)
        glutInitWindowPosition(300, 200)
        glutCreateWindow('Quidam Of OpenGL')
        
        init()                              # 初始化画布
        glutDisplayFunc(draw)               # 注册回调函数draw()
        glutReshapeFunc(reshape)            # 注册响应窗口改变的函数reshape()
        glutMouseFunc(mouseclick)           # 注册响应鼠标点击的函数mouseclick()
        glutMotionFunc(mousemotion)         # 注册响应鼠标拖拽的函数mousemotion()
        glutKeyboardFunc(keydown)           # 注册键盘输入的函数keydown()
        
        glutMainLoop()                      # 进入glut主循环    
    

    4.11 小结

    虽然还有很多领域需要我们继续探索,比如灯光、材质、雾化、拾取等,但那不是奇幻之旅的目标。奇幻之旅仅仅是帮助读者建立 OpenGL 的基本概念。至此,我们基本完成了任务。

    5 加速渲染

    实际应用 OpenGL 绘制三维图像时,往往需要处理数以万计的顶点,有时甚至是百万级、千万级。我们通常不会在绘制函数里面传送这些数据,而是在绘制之前,将这些数据提前传送到GPU。绘制函数每次绘制时,只需要从GPU的缓存中取出数据即可,极大地提高了效率。这个机制地实现,依赖于顶点缓冲区对象(Vertex Buffer Object),简称VBO。

    尽管 VBO 是显卡的扩展,其实没有用到GPU运算,也就是说 VBO 不用写着色语言,直接用opengl函数就可以调用,主要目的是用于加快渲染的速。

    VBO 将顶点信息放到 GPU 中,GPU 在渲染时去缓存中取数据,二者中间的桥梁是 GL-Context。GL-Context 整个程序一般只有一个,所以如果一个渲染流程里有两份不同的绘制代码,GL-context 就负责在他们之间进行切换。这也是为什么要在渲染过程中,在每份绘制代码之中会有 glBindbuffer、glEnableVertexAttribArray、glVertexAttribPointer。如果把这些都放到初始化时候完成,使用一种结构记录该次绘制所需要的所有 VBO 所需信息,把它保存到 VBO特定位置,绘制的时候直接在这个位置取信息绘制,会简化渲染流程、提升渲染速度。这就是 VAO 概念产生的初衷。

    VAO 的全名是 Vertex Array Object,首先,它不是 Buffer-Object,所以不用作存储数据;其次,它针对“顶点”而言,也就是说它跟“顶点的绘制”息息相关。VAO 记录的是一次绘制中所需要的信息,这包括“数据在哪里 glBindBuffer”、“数据的格式是怎么样的 glVertexAttribPointer”、shader-attribute 的 location 的启用 glEnableVertexAttribArray。

    根据我查到的资料,几乎所有的显卡都支持 VBO,但不是所有的显卡都支持 VAO,而 VAO 仅仅是优化了 VBO 的使用方法,对于加速并没有实质性的影响,因此本文只讨论 VBO 技术。

    5.1 创建顶点缓冲区对象(VBO)

    假定画一个六面体,顶点是这样的:

    # 六面体数据
    # ------------------------------------------------------
    #    v4----- v5
    #   /|      /|
    #  v0------v1|
    #  | |     | |
    #  | v7----|-v6
    #  |/      |/
    #  v3------v2
    
    # 顶点集
    vertices = np.array([
        -0.5, 0.5, 0.5,   0.5, 0.5, 0.5,   0.5, -0.5, 0.5,   -0.5, -0.5, 0.5, # v0-v1-v2-v3
        -0.5, 0.5, -0.5,  0.5, 0.5, -0.5,  0.5, -0.5, -0.5,  -0.5, -0.5, -0.5 # v4-v5-v6-v7
    ], dtype=np.float32)
    
    # 索引集
    indices = np.array([
        0, 1, 2, 3, # v0-v1-v2-v3 (front)
        4, 5, 1, 0, # v4-v5-v1-v0 (top)
        3, 2, 6, 7, # v3-v2-v6-v7 (bottom)
        5, 4, 7, 6, # v5-v4-v7-v6 (back)
        1, 5, 6, 2, # v1-v5-v6-v2 (right)
        4, 0, 3, 7  # v4-v0-v3-v7 (left)
    ], dtype=np.int)
    

    在GPU上创建VBO如下:

    from OpenGL.arrays import vbo
    
    vbo_vertices = vbo.VBO(vertices)
    vbo_indices = vbo.VBO(indices, target=GL_ELEMENT_ARRAY_BUFFER)
    

    创建 顶点 VBO 时,默认 target=GL_ARRAY_BUFFER, 而创建索引 VBO 时,target=GL_ELEMENT_ARRAY_BUFFER,因为顶点的数据类型是 np.float32,索引的数据类型是np.int。

    在VBO保存的顶点数据集,除了顶点信息外,还可以包含颜色、法线、纹理等数据,这就是顶点混合数组的概念。假定我们在上面的顶点集中增加每个顶点的颜色,则可以写成这样:

    vertices = np.array([
        0.3, 0.6, 0.9, -0.35, 0.35, 0.35,   # c0-v0
        0.6, 0.9, 0.3, 0.35, 0.35, 0.35,    # c1-v1
        0.9, 0.3, 0.6, 0.35, -0.35, 0.35,   # c2-v2 
        0.3, 0.9, 0.6, -0.35, -0.35, 0.35,  # c3-v3 
        0.6, 0.3, 0.9, -0.35, 0.35, -0.35,  # c4-v4 
        0.9, 0.6, 0.3, 0.35, 0.35, -0.35,   # c5-v5 
    	0.3, 0.9, 0.9, 0.35, -0.35, -0.35,  # c6-v6 
    	0.9, 0.9, 0.3, -0.35, -0.35, -0.35  # c7-v7
    ], dtype=np.float32)
    

    5.2 分离顶点混合数组

    使用 glInterleavedArrays() 函数可以从顶点混合数组中分离顶点、颜色、法线和纹理。比如,对只包含顶点信息的顶点混合数组:

    vbo_indices.bind()
    glInterleavedArrays(GL_V3F, 0, None)
    

    如果顶点混合数组包含了颜色和顶点信息:

    vbo_indices.bind()
    glInterleavedArrays(GL_C3F_V3F, 0, None)
    

    glInterleavedArrays() 函数第一个参数总共有14个选项,分别是:

    • GL_V2F
    • GL_V3F
    • GL_C4UB_V2F
    • GL_C4UB_V3F
    • GL_C3F_V3F
    • GL_N3F_V3F
    • GL_C4F_N3F_V3F
    • GL_T2F_V3F
    • GL_T4F_V4F
    • GL_T2F_C4UB_V3F
    • GL_T2F_C3F_V3F
    • GL_T2F_N3F_V3F
    • GL_T2F_C4F_N3F_V3F
    • GL_T4F_C4F_N3F_V4F

    5.3 使用顶点缓冲区对象(VBO)

    使用glDrawElements() 等函数绘制前,需要先绑定顶点数据集和索引数据集,然后使用glInterleavedArrays() 分理出顶点、颜色、法线等数据。

    vbo_indices.bind()
    glInterleavedArrays(GL_V3F, 0, None)
    vbo_indices.bind()
    glDrawElements(GL_QUADS, int(vbo_indices .size/4), GL_UNSIGNED_INT, None)
    vbo_indices.unbind()
    vbo_indices.unbind()
    

    6 致谢

    写作过程中,我参考了很多资料,包括纸质书籍和网页,列写于此,一并致谢!

    1. 《OpenGL编程精粹》杨柏林 陈根浪 徐静 编著
    2. Opengl开发库介绍
    3. OpenGL的API函数使用手册
    4. glut处理鼠标事件
    5. Learn OpenGL

    本文写作过程持续了两三周,期间可谓呕心沥血。发表之际,如释重负,填词以记之。

    愁云淡淡风萧萧,
    暮暮复朝朝。
    别来应是,体重锐减,骨立形销。
    
    日日思OpenGL,
    情绪好无聊。
    一篇博文,一台电脑,一只鼠标。
    
    ——调寄《眼儿媚》
    

    7. 后记

    近期有很多朋友通过私信咨询有关Python学习问题。为便于交流,我在CSDN的app上创建了“Python作业辅导”大本营,面向Python初学者,为大家提供咨询服务、辅导Python作业。欢迎有兴趣的同学使用微信扫码加入。

    在这里插入图片描述

    从博客到公众号,每一篇、每一题、每一句、每一行代码,都坚持原创,绝不复制抄袭,这是我坚守的原则。如果喜欢,请关注我的微信公众号“Python作业辅导员”。

    在这里插入图片描述

    展开全文
  • 一手好字:硬笔书法轻松自学指南(知乎周刊 Plus) 知乎编辑团队 楷书,认知好字的范本 2017-03-16 《黄自元间架结构九十二法》 选本好字帖 2017-03-16 先谈书体。 前人对练习书法的程序,各有主张。有的认为...

    写一手好字:硬笔书法轻松自学指南(知乎周刊 Plus)

    知乎编辑团队

     楷书,认知好字的范本
     2017-03-16
    《黄自元间架结构九十二法》

     选本好字帖
     2017-03-16
    先谈书体。
    前人对练习书法的程序,各有主张。有的认为应由书体起源篆—隶—楷—行—草的演变顺序学习,有的主张先练楷书,再追上篆隶。不过篆隶在今日不会日常使用,所以不打算深入书法的朋友不用考虑。那么就是楷书和行书的书体顺序问题了。
    楷书结构严谨,笔画规范。从基本的端庄开始练起,比较实际。行书与楷书虽然差别不大,但行书笔画的起止转承,抑扬顿挫都是跟楷书一脉相承,没有楷书的基础,行书并不让人感觉清爽。楷书写得行些,就是行书行楷;行书写得草些,就是草书行草。楷书练好了,练行书是很轻易的事情。
    所以建议,以楷书为主,行书为辅,同时练起。
     2017-03-16
    书体确定了,现在来谈书派。
    同是楷书,各派的面貌都不一样。我建议,先临摹一两个月现代人的硬笔楷书如《田英章硬笔楷书》作为基础,再选用古帖临字。选好一位大家之后,就要专心临写,不要见异思迁。比如选定的是欧阳询的《九成宫醴泉铭》,就要按照「摹」「临」的方法,一步步练下去,直到意临。这时欧体的基础已经打得很扎实。如有意继续提高巩固,另开一新帖,则最好从欧体其他的帖子《皇甫君碑》《虞温恭公碑》等帖中继续巩固学习欧派楷书。然后按照同样的方法学习欧派行书。
     2017-03-16
    附上一些精良的字帖和书法大家(部分经典之作如《兰亭序》《胆巴碑》《九成宫碑》《多宝塔碑》等都有自带米字格和分析详解的书,淘宝或者书店都可以买到。):
    楷书:
    钟绍京《灵飞经》
    赵孟頫《道德经》《胆巴碑》《汲黯传》
    文徵明《落花诗卷》《老子列传》
    王羲之《乐毅论》《黄庭经》
    颜真卿《多宝塔碑》
    欧阳询《九成宫醴泉铭》
    柳公权《神策军碑》《玄秘塔碑》
    王献之《洛神赋》
    行书:
    苏轼《黄州寒食诗帖》
    米芾《蜀素帖》
    陆柬之《文赋》赵孟頫《千字文》
    王羲之《兰亭序》
     2017-03-16
    楷书字体也有很多,找你喜欢的去选,这里罗列一些供大家选择:
    钟繇(正书之祖)《荐季直表》《宣示表》;
    王献之《洛神赋》;
    柳公权《神策军碑》《玄秘塔碑》;
    欧阳询《九成宫醴泉铭》;
    褚遂良《雁塔圣教序》;
    颜真卿《多宝塔》;姜夔《跋王献之保母帖》;
    文徵明《太上老君说常清静经》;
    赵孟頫《汉汲黯传》;
    钟绍京《灵飞经》;
    赵孟頫《胆巴碑》;
    个人建议选择规整的、法度严谨的楷书作为这个阶段的练习,能打下极好的结构基础。虽然上述字帖后面的都是小楷,但新手很难发现其中的结构规律并加以利用,长久看来易导致字轻浮,因功底不实。
    着重推荐《欧体楷书间架结构:九十二法字帖》,很薄,价格便宜,方便携带,实为初学楷书之至宝。
     2017-03-16
    需要着重强调的是,个人认为在初学硬笔阶段一味地追求写出毛笔字的粗细变化,要求达到很高的笔画表现力而忽视结构,这是一个舍本逐末的事情!现在特别多的人推崇这种极力用圆珠笔中性笔表现毛笔的粗细的方式,我不否认这是艺术,但是不建议新手这么做,浪费时间!
     2017-03-16
    如果可以解锁行书了。这时可以选择上述一些笔画灵动的字帖,比如《灵飞经》及《胆巴碑》等,然后过渡到:
    王羲之《兰亭序》《圣教序》;
    苏轼《黄州寒食诗帖》《前赤壁赋》;
    米芾《蜀素帖》。
     2017-03-16
    从今天起选一页字帖(毛笔的),每天精读三次(不是朗读,而是仔细观察帖子中的每一个细节,帖子例字是如何运笔的,结构是怎样的,等等),每次15分钟,每天仅需训练两三个字,一个月后写字的手感也是完全不一样的。这些我曾经在学生身上试验过,对培养感觉相当有帮助。
     2017-03-17
    比如这个陆柬之的「不」字:
     2017-03-17
    横:向左起笔轻轻折向右下,然后平铺,最后向上微扬折向右下重按回锋收笔
    撇:轻向上提笔右下折(可省略成直接右下折),然后往左展开
    竖:重按右下折之后竖直下来,最后提按回收点:重点在最后的提按回收

     选一支顺手的笔
     2017-03-17
    在实际书写中,常用的笔芯则应该是HB、B与2B三种了。
    在硬笔书法书写过程中,铅笔的最大特点在于表现力强。哪怕是同一支铅笔,用力不同,写出来的线条颜色深浅程度也不同。换句话来说,它除了能够表现出笔画的粗细变化,还能表现出色泽的深浅变化。因此,铅笔的笔触层次感更强,所能表现的肌理远比其他同类书写工具丰富。
     2017-03-17
    中性笔——性价比高,易得性强,习惯了还挺好写的
    似乎在不知不觉间,中性笔已经成为近年书写工具界的新秀了。一般来说,中性笔出墨流畅,笔画轻重便于控制;因此上手容易,操控性强。
    相对而言,中性笔不尽如人意的地方在于笔触未能体现浓淡变化,而且笔触的粗细变化相较钢笔要小。因为制作工艺所限,一般超过0.5cm的中性笔笔芯表现能力就变弱了,没有办法随心所欲地写出似有还无的牵丝效果。这个特性也限制了中性笔书写字体的大小,写出来的字普遍略微偏小,不适合表现单字的细部。
     2017-03-17
    书写的过程,本身就是书写者和书写工具不断磨合的过程。钢笔的使用寿命较长,在和使用者相磨合的步骤中,具备了天然的优势。
    由于其笔杆材质多为金属,使用的时候感觉分外厚重;当钢笔在纸面上挥洒的时候,常有种执长枪巨剑驰骋沙场的快意。
    另外个人认为,明嘴钢笔因为弹性更足,它的使用效果相较暗尖的更好,而且暗尖钢笔的笔头较粗,一定程度上会遮挡住书写者的视线,不便于书写。
     2017-03-17
    如果是为了日常记录且写中文,就不要买德系、美系的笔——包括万宝龙、百利金、Lamy和派克——因为这些笔的特点是粗、不容易出锋、下水过于顺畅,写小字会很痛苦。至于Lamy EF,最近好像不出了,而且「不划纸的EF不是合格的EF」。
    如果你不玩钢笔,就不要入老笔,包括老版百利金、派克、万宝龙以及国产笔——它们虽然好写但是需要花工夫先研究下钢笔本身。
    所以日系笔才是你最好的选择,包括百乐78G,属于物美价廉且好写(中文)的典型代表,价格不过百。
    日系笔一共有三个牌子:百乐、白金和写乐。百乐78G已经说过了;白金3776是其代表作,不想价格太贵可以选择钢尖的3776;写乐比较出名的是21K,不过价格过千了(钢尖的我不太清楚),所以如果预算充足的话可以考虑。
    百乐的金笔是这三个里面最软的(钢尖的我也没有用过所以不太清楚),以前买过百乐74,太软了驾驭不了,现在已经让它颐养天年了……
    日系笔的笔尖大小,选择EF或者F甚至M都是可以的,拿来写中文做记录都没太大问题。如果你买百乐74,请选F尖及以下,由于其笔尖较软,我个人觉得M有些粗。
    再说说国笔。对于英雄近几年出的现代笔,个人认为还是不要浪费钱了。至于毕加索一类的,还是算了,使用上面不太靠谱。而国产老笔,上文已经评价过了「虽然好写但是需要花工夫先研究下钢笔本身」。

     从正确的执笔方法开始
     2017-03-17
    有些要点图中没有提到,但就是这些细节决定了你运笔时候的舒展和流畅:
    1. 笔和纸之间的夹角约在50—75度之间。
    2. 执笔时候,不应当用拇指搭住食指,或者用食指搭住拇指。拇指食指间应保持若即若离的状态。
    3. 食指指腹,拇指指腹右半部,中指第一指间关节和食指掌指关节与笔接触;
    4. 食指指腹,拇指指腹右半部为主要发力点,中指第一指间关节为次要发力点,食指掌指关节作为定位用。
     2017-03-17
    在书写「钩」之类自右下到左上的笔画的时候,无名指应该协助中指发力提钩,不宜软趴趴地枕在纸面。
    书写过程中,手指应该是舒展自然的,绝不能把笔掐紧,硬邦邦地跟笔较劲。
    因为手指握笔的时候,为笔提供的仅仅是对笔管的压力,在垂直于纸面的方向上,这个压力仅仅影响手指对笔的最大静摩擦力。而笔的静摩擦力,只要与笔的重力和纸面对笔的弹力两者合力相互抵消就足够了。无论人怎么用力握笔,都不会影响这个竖直面的受力情况;反而会因为握笔过分用力,写起字来就会不够灵活、运笔艰难,写出来的字显得生硬而没有灵性。
    以上,是握铅笔的方法。
     2017-03-17
    握中性笔、签字笔等水笔的方法和握铅笔方法基本相同。但由于中性笔是通过液体毛细作用输运墨水到纸面的,比较容易写出笔画,所以握笔的力度可以变得更加轻。上面的执笔图中所提及的第二点,「食指两段弯曲,勿三段弯」的说法可以忽略。
    握钢笔的执笔方法和握铅笔的方法不同。其不同点在于,由于铅笔、中性笔在书写时,对纸面的有效作用力是垂直于纸面的;而钢笔则是通过压迫笔尖的钢片,使笔尖发生形变,墨水从钢片间注入到纸上的。所以在钢笔书写过程中,有效的作用力应该是使钢片产生形变的力,而不是垂直于纸面的力。
    如图示:
    (图源见水印)
    因此,钢笔的执笔要领是:
    笔杆应靠在虎口处;而不是像之前握铅笔的时候一样靠在食指掌指关节处。
    笔和纸的夹角约为40—65度。
     2017-03-17
    执笔要做到「指实掌虚」,就是下指(拇指和中指)握笔要实,掌心要空,空若蛋形。手指离笔尖太近,执笔角度大,写不好大字。离笔尖太远,写不好小字。拇指包住笔杆,运笔不灵活,大拇指与食指(虚握)轻挨着就好。握笔不要太用力,不然食指前段就是向内撇的。

     所谓诀窍:眼到、心到、手到
     2017-03-17
    正经说来,练字的要义不过三点:
    眼到、心到、手到
    在练字的过程中,先要肉眼感知到字的笔画特征、字形的间隔结构;而后当用心体察笔势流动,揣摩笔毫走向;继而应之于手,在自己的笔下再现出来。
    但在实际的训练上,多数人只看重手到,一股脑地把一个字重复写上数十遍,却没有仔细体察原字帖中,字形的精妙之处。最终不过用别人的字帖,写自己的字而已。纵然努力,亦不过是在走着一条事倍功半的路。
    其实,书法与诸多其他技艺一样,最是在无处见功夫。那些不能直接展示于人前的地方,反而需要花更大心力。我在练字那段时间里,写字倒不算太多,但行路搭地铁之时,心闲下来了,字帖上的字就自动地浮到眼前。
    用手者,不过匠人;用心者,方能体察其妙处。

     练习书法有几种方法
     2017-03-17
    如何练字,方法千奇百种,随君喜好;
    而如果问如何「系统」地练字,就不劳大家原创了,古人已经给我们留下了很传统和好用的方法:
    「读、摹、临、背、意、用」,循序渐进。
    我经历过很多渠道来学习书法,无论是长辈教,自己练,还是书法班,要系统渐进,都脱不了这六个字。不管自学还是报班,都要经过这样的过程,甚至很多时候,书法班能教给你的,从「背」以后就不会有了。没办法啊!修行在个人,就算是老师也还要不断地进行「临」「背」「意」「用」呢。下面分别来说一下。
    读:读帖,读字。这个只要有作品,就可以去读。对联,屏风,匾,摩崖石刻,生活中可见到的有很多。初学书法,总得选择字帖吧?选字帖时你就是在读帖了。然后选自己喜欢的,或者负责任的老师建议你学的。读帖不一定要能读出长篇大论,重要的是「有印象」。
    摹:就是用透明的白纸蒙在字帖上照着描写。这里说一下这个步骤的必要性:这个是初学书法者必须要做的。有的人说摹不行,不能充分思考字意,完全掌握。我想说初学者最重要的是「矫正字形」,而不是「研究风格」。先练好间架结构,再去追求个人风格,所以首先让自己把笔画放在正确的位置上吧。这个阶段最重要的是追求「像」,即「形似」。如果字还没写正就进行临写,这样去达到「像」的目的,反而会破坏你对书法写意的印象。临是学意,不是学形。而且那不是在写字,而是在画画儿,像在画一个字似的。再像又有什么用?虽说摹不易记字,但是能保证你写字的数量。初学需广泛涉猎,把所有结构都见过描过,临起来才有感觉,有体会,能融入自己的想法。
    当临写不同风格的字帖,如果感觉就像「大换血」时,也要从摹重新开始。
    临:需要一定的基础再进行。忌看一画,写一画。但初学者没有基础,临起来无从下手,就会出现这种现象。一定要看一字写一字,最好看一句写一句。临写能写出帖的笔意,虽然会不像,但是会「神似」。这就是临追求的效果。临写是学习时间最长的过程,也是必经之路。
    背:背临。疏通整个字帖后,抛去,凭直觉去写。有时候临得多了,自己就会背了。我临写《兰亭集序》时是五年级、六年级,临了两年,一共就那么多字,以至于里头的文章我还没学会断句呢就背下来了。初中看到这篇古文,看着书上的铅字,脑子里都是还原成软笔字帖时的字形。背临还是自然而然最好,这是我的想法。
    意:意临,背临的基础上再创造,融入自己的风格。需要有自己的风格,最起码得熟练两种以上的字体吧?达到这个程度,就活用了字帖了,这个阶段就可以独立去出一些作品了。最起码,给邻居写个对联,墙头贴个告示,都是信手拈来的事儿。
    用:其实上面说的写对联,贴告示就是在用。书法源自生活,学习到一定阶段后,就可以拿来服务于生活了。

     为什么劝你扔掉描红帖
     2017-03-20
    你可能会说,「这不就是连笔么?」连笔正是体现书写性的一种方式(当然连笔写得丑那是另外一个问题了),为了确保文字的高可读性,通常不会让每个笔画之间都「连笔」,但是这种连带关系依然在空中发生,这意味着你的手在空中依然要沿着笔画的自然书写轨迹去运动,「笔断意连」是矣。
    这种依靠手势去自然地衔接笔画确实会让你觉得把字写好其实没有那么困难。很多人能做到局部的「首尾呼应」,但是对于那些习惯搭字的人来说,要做到每个字都能有这种自然书写的「连带关系」,可能就要在书写的时候多提醒自己。
    还有很多人字写得丑,是因为他们要么把字写得像火柴棍那样硬邦邦,要么像用久了的鞋带那样绵软无力。由于一般人写的硬笔字可能并不大,那么通常需要动这两个部位:「手腕和手指」,手腕的活动半径是要比手指大很多的,因而尽管看起来只要手指动起来就能写字,但真实情况是手腕去带动手指完成书写。尤其是在处理长笔画的时候,不用手腕是不可能完成流畅的书写。
    如果你真正有去下意识地观察,其实你会发现真正依靠这两个地方很自然地书写的时候,是压根做不到「横平竖直」的。
     2017-03-20
    所谓的「横平竖直」在这当中并不多见,强行去追求「横平竖直」的必然结果就是把笔画写得像火柴棍,毫无灵动可言。所以正常的书写习惯是会有带弧度的,这种弧度往往也是美感的体现。
    而把笔画写得像鞋带那样绵软无力的,通常是因为不懂得用手腕去带动手指去书写。记住,是用手腕去控制手指,而不是让手腕去受制于手指,甚至于有些时候要动用手臂。
     2017-03-20
    写「好字」,不一定非要去强行地全部模仿别人的所有书写模式和习惯(描红),这是非常低效率和枯燥的。因为人和人之间的书写模式总是不尽相同的,哪怕看起来一样,也总有很多细微的差异。而我认为如果目的是「写|好字」,只要给书写者搭上一个好的框架就可以了,剩下的细枝末节书写者自己完成即可。而这点,恰恰是「描红」做不到的事。「授人以鱼不如授人以渔」,与其痛苦地模仿每个字,不如去认识到应该怎样省时省力地去书写「好字」。
    当然,上文我只是提到了正确的用笔方式是怎么样的,而「好字」,除了有正确的用笔方式,还有各式各样的结构组成,而这,我认为才是硬笔字写好最困难的部分。赵孟頫在他的《定武兰亭跋》中是提到:
    书法以用笔为上,而结字亦须用工,盖结字因时相传,用笔千古不易。
     2017-03-20
    我从书法字典网上摘取的「树」这个字不同时期不同人的写法。可以看到各家采取的结构,要么拉长字的上半部分,要么就拉长字的下半部分将重心下移。
    为什么会这样做?如果拉长字的上半部分,会使得字的纵势感变得很强烈,整个字显得非常的高挑。
     2017-03-20
    除了重心,对部首与部首、笔画与笔画之间的位置关系也是值得去学习的。部首的穿插、避让,笔画长短、位置都值得去归纳分析。
     2017-03-20
    主笔的舒展。
    一般只要你的笔画写得不像火柴棍那样,你的字便更多是结字的问题。
    和现在常用的宋体与黑体不同,古人在写楷书的时候很少把字处理得正正方方的。
    孙过庭在《书谱》中这样说道:
    初学分布,但求平正;既知平正,勿追险绝;既能险绝,复归平正。
    初学者,学字,不求有功,但求无过。把字写得正正方方的是绝对没有问题,同时也是必要的。所以如果你写的字像宋体和黑体那样,虽然说可能不够生动与艺术性,但是绝对是和丑挂不上钩的。
    入门后,为了让字更具生动与神采,往往会在用笔和结构上追求一些变化,试图寻找一种动态的平衡,这也就是古人所谓的「险崛中见平正」,这点,中国钢笔字一哥田英章学习的对象欧阳询做到了极致。
     2017-03-20
    所谓的「主笔伸展」是在压缩其他笔画的空间后,对一个字的「主笔画」进行夸张的处理。准确意义上来说应当是「有伸必有缩」,当然,这就考验到对结构的驾驭能力了。
    事实上,这种「主笔伸展」的字再好写不过,因为你写这类字,只要把所谓的「主笔」进行一定程度的夸张(通常是延长笔画的长度),那你至少可以拿60分。
     2017-03-20
    当然很多人都知道这个笔画的主笔都在右下部分的「戈」部的那笔。如果你记住了这点,再写其他带有「戈」部的时候,特意强调这个主笔便是非常明智的。这种「强调主笔」算是所有结字方法中受益最大的一种,如同万金油,各位可以自己去尝试一下。
    如果要更进一步的话,我对「羲」在整个字形上的处理就会采取更加大胆和灵活的方式,通过对字的倚侧、笔画的伸缩,来达到一种新的姿态,而不能仅凭「强调主笔」这一种办法。但是,无论如何处理,这个字始终是正的,它的重心依然是稳当的。

     先点画结体,后章法布局
     2017-03-20
    第一,仔细观察相同点画在不同字之间的变化,在字当中不同位置的写法(比如各种「点」「撇」),把这些点画位置不同的字抽出来,进行练习,熟悉它们在不同位置怎么写,直到熟练运用。
    第二,寻找代表性结构字,意为,在各种间架结构里,每种结构的代表字。如「安」「每」「女」「永」「乃」「及」「宫」「赢」等等,写好其一,事半功倍。临帖并非不动脑子,反而是需要十足的耐心和专注,把这些字抽出来,临写到尽可能像。
    第三,对字帖很熟悉后,你需要「背临」,指扔掉字帖靠记忆去写。最后是「意临」,即掌握其主要风貌,融入自己的风格。最后两项一样重要,否则你无法脱离字帖去写其他东西。
    临写练习时,把你的精神专注在笔下,重现前人一点一画的笔触,再造一字一行的结构布局,乃是以谦逊及诚恪的态度,好奇与追求的心,去体会这种高度抽象的美,无论其风格是雄强的,或是秀美的,是昂扬的,或沉郁的,都值得你暂时扔掉手机离开电脑去沉浸其中。抱着急功近利的心态写不好字。
     2017-03-20
    后章法布局
    硬笔书法章法不同于毛笔书法章法,盖因古今习惯不同。应用文体书写一般遵从自左向右的书写格式,应遵循下面几条:
    (1)字整体造型宜扁不宜长(竖画不宜过分伸长,笔势取横向)
    (2)勿紧贴横格上下线(适中位置应沿横格中心线,没有把握更不要超格)(3)标点符号空半格(节奏感)
    (4)字大小适中,首字要写好
    抱着日常使用目的的你,看到这里就足够了,回去练字吧。

     如何提升笔画的质感
     2017-03-20
    1. 轻重
    在书法当中,无论是楷书、行书,抑或草书,都应当对笔画的轻重变化,给予高度的重视。缺乏轻重变化的这几种书体,必然难以具备足够的表现力,势必呆板而俗不可耐。
     2017-03-20
    行书当中,为了书写连贯流畅,常将基本笔画(点、横、竖、撇、捺、钩、折、挑)用牵丝的形式进行相互勾连。在书写的过程中,需注意基本笔画应较为粗壮,牵丝则应轻细。这样才能保证字形交代清楚,笔画主次分明。
     2017-03-20
    常见的构成笔画间呼应的一般以下有三种书写技法:
    2.1 笔连
    在不影响字形的前提下,将收笔起笔处相近的笔画连在一起。如下图所示,「知」字首笔「撇」与第二笔「横」,以及倒数第二笔「横折」与末笔「横」,皆是通过笔连的形式连接起来的。这种写法,简洁明了。通过提按和转折关系区分两笔,通过相互连接的位置关系形成呼应。亦可简化起笔收笔步骤,增强字中的流动感。
    2.2 牵丝
    牵丝主要为了勾连距离较远的两个笔画,出现于前后笔画间,标识了运笔的路径。如前所述,牵丝需要做到的是轻细,保证两个基本笔画之间界限分明。如下图「知」字左部「矢」与右部「口」即是通过牵丝连接。
    牵丝多由于行笔速度加快,前笔书尽,而未完全离纸即书写下一笔画所形成,多为自然生发。牵丝多,则显乱。初学者使用牵丝当注意有所节制,宜少不宜多。
    2.3 意连
    意连指的是书写过程中,笔画之间的连接,没有直接体现在字上,但通过「意」和「势」相互连接了起来。最简单的意连方法,就是把本来可以用牵丝写就的地方,切断牵丝,只留下出锋和入笔,留点相互连接的意思。如下图「知」字第二笔、第三笔两个横画,便是用这种方法连接起来的。为便于理解,特用红色虚线标识笔势。
    意连的妙处在于似有实无、含而不露、藏而不发、窥虚见实、于无处见有。
    楷书中,意连出现得更多,但多以笔意相贯、结构趋就而成,基本不出现之前所说的切断牵丝的简单做法。如图中,「乎」字即为楷书。未能得其精要,只能揭示意连之诀一二。
     2017-03-20
    3. 章法
    呼应可以出现在字中两笔画之间,也可以出现在两个相连的字之间。
     2017-03-20
    就单个字来看,大体的书写方向是从上到下,从左到右的。末笔所处位置,绝大多数位于底部或右下角。在行书当中,竖写时,若末笔为撇或竖等适宜直接出锋的,笔锋放出后,便可顺势连接至下字之首画;若末笔为横或点等以顿笔做结的,写至最后,笔势如被压得最紧的弹簧,重重顿下,然后撤去这股势劲,笔锋便自然爽朗地往回出锋了,同样能自如顺畅地衔接下一个字。譬如上图中,「请」字的出锋便是一证。末笔为捺者,需做呼应时,常改为长点;末笔为钩者,可省略钩。
    就此可以看出,因为汉字具有的天然特点,竖写的作品,常常在各种情况下,都能保证字与字之间相互联系,相互呼应。因此,传统书法中,都要求竖排从右到左书写。
    与这种书写格式对应的,是传统书法对「行气」的重视。
    「行气」念作「ㄏㄤˋㄑㄧˋ」(hang·qi)。
    意指书法作品中,一行字需以一气贯之。其基本要求有二:一则是在一行字当中,能拉出一条中轴线,保证轴线左右两边的视觉重量大致相等;另一则是字与字间具有相互联系,不致各自为政,支零破碎。
    行气的概念是从书法作品的整体效果出发提出的概念。这些从整体布局谋篇出发提出的书写要求被统称为章法。若是自己戏写数字,自是没什么章法的要求的。但在正规的书法创作中,则需遵守一些章法上基本的要求,以免贻笑大方的。
    一般传统书法要求竖排从右到左书写,先有正文,后作落款,款字较正文略小。由于繁体字是自隋唐一路传承演进而来,有着一套成熟的书写技法和审美情趣,故传统书法以繁体字写就。个人认为,以简体字书写亦无不可,只是简体字出现未满百年,如何结字构型尚处于摸索阶段,可能会损失部分美感。而书法作为一门艺术,其美学意义当是高于实用意义的。因此,即便用简体字写就书法作品无可非议,个人依旧不做推荐。而尤为需要注意的是,切莫繁简混杂,以避免突兀之感。

     间架结构才是字的风骨
     2017-03-20
    《黄自元间架结构九十二法》这本书,系统、全面地研究剖析了汉字结构组合规律,归纳总结出九十二种汉字结体书写的方法,他的楷书集欧、颜、柳三家之长,值得初学者学习临摹。
     2017-03-20
    画短撇长横短撇长。如:左、在、尤、龙
    画长撇短横长撇短。如:右、有、灰
    画短直长,撇捺宜伸横画短竖画长的字,撇捺应延伸。如:木、本、朱
    画长直短,撇捺宜缩横画长竖画短的字,撇捺应缩短为相背的点,两相呼应。如:乐、集两平者,左右宜均由两个相等部分组成的字,左右要均匀。如:颜、顾、騵
    三合者,中间务正由三部分组成的字,中间务正。如:御、谢、树
    两段者,上下平分中微加饶减由上下两部分构成的字,上下两部分各占一半,中间稍加变化。需、留
    三联者,头尾伸缩间仍要停匀由上中下三部分组成的字,头和尾伸缩要得当,突出主笔,协调统一。如:章、意、素、累
    掌握了以上的一些规律,会发现写的字有质的提升。
     2017-03-20
    读帖就读这三样:用笔,结字,章法。
     2017-03-20
    1.1 读用笔
    用笔事关个人风格,喜欢粗细对比鲜明的字,还是刚直锋利的字,或者飘逸延绵的字,都是自己的选择,只要符合书法美学,都可以有上乘佳作。既然选择阅读一则帖,则就要从欣赏的角度去看,抛去自己先入为主的个人风格。
    然后,怎么看出个人风格?
    举个例子,看看这个「呼」字的一钩。直接出锋,但不猛烈。想起了谁的字?
    我再上几张图就知道了。
    左边的「乎」是欧阳询的《九成宫醴泉铭》;右边的「事」是王羲之的《兰亭集序》。
    再看看作者生平,是不是临习过这二者的书法?(实际上欧阳询也临习过王羲之的字)
    诸如此类。
    然后,怎么再在读帖中学习?
    眼观帖,心中写。读的毕竟是字帖,不是文章。不能看字认识然后说「真好看啊」就看完了。自己要在心里把字过一遍,随着字的笔画顺序,起承转合,写在心里。何时停顿,何时转折,下笔轻重如何,这些都可以在心里完成。
    一开始可以拿手指头模仿笔去比画,管用。我一开始学书法半夜睡不着还在被子上没事拿手指瞎画来着。
    1.2 读结字
    结字就是间架结构。
    这个虽说也有关于个人风格,比如颜体饱满结实的稳重结字,和刚直紧凑的柳体字就感觉不一样,但是间架结构不差上下。如果是初学者,最好感受一下这些楷书严谨的结字,多对比一些唐楷,找这些作品的「相同点」,然后模仿学习,促进自己字形的矫正。而如果是熟家大师,则不需要「矫正」这个步骤了,这时候欣赏这些楷书,更多是看一些「不同点」,然后思考与总结。初学者千万别像大师一样追求「奇」,一定要总结「同」。
    比如上面一开始的这两个字,初学者要想:
    「知」的右边口怎么放置较好,「呼」的左边口呢?
    黄自元的帖子不劳烦各位读者来自己总结了——「右边少者齐其下」「左旁少者齐其上」。
    这便是这本字帖的伟大之处。

     读帖:离开字帖也能写好看的秘诀
     2017-03-21
    后面图片全部来自《九成宫醴泉铭》
    1. 重心
    【这个字的重心是哪一笔?】
    由于楷书的横都有上斜的趋势,因此要有一个笔画把字拉正,这就是字的重心。一般来说字的重心:笔画向下或者斜向右下/位于字的右半边/(第二个)折肩或者捺(捺化成的点)。
    画圈的笔画即为重心:
    2. 主笔及疏密
    【这个字的主笔是哪一笔?】
    主笔即为一个字中最为伸展的笔画。一般来说独体字和上下结构的字找最长的,左右结构的左边是部首的主笔在右边,右边是部首的左右各有一笔伸展。有的字有次主笔,有的字主笔不明显。
    【这个字的疏密关系是什么?】
    找到主笔疏密就很容易找了,伸展出去的地方为疏,中间笔画密集的地方为密。疏密关系着该字中宫是否紧凑,很重要。
    画圈的笔画为主笔:
    3. 轻重及间隔
    【这个字中每一笔的轻重是怎样的?】
    有些字帖轻重笔画变化明显,比如张即之,有些则相对不那么明显,比如瘦金。但是没有字帖是要把笔画写平均的。很多人的字平放在桌子上看还可以,但是挂起来会显得特别平,没有跳动感,就是因为轻重变化注意得不够。
    【这个字间隔是如何排布的?】
    横向间隔一般来讲是均匀的,字中间的点一般算一横来排布,也就是说点算作两个间隔,如图所示:4. 左右错落
    【左右结构的字如何摆布?】
    【起笔的相对位置在哪里?】
    【部首的相对大小是什么?】
    再强调一次,关于结构的分析只是给大家提供一种方法,找主笔找重心都是为临帖服务的,不要陷入其中。如果很容易就能把字写得与字帖九分像,不分析也可。三、章法:整体中各个字的相互关系
    【字与字之间的轻重变化是什么?】
    不仅一个字之中有轻重变化,字与字之间也有轻重的变化。
    【字与字之间的大小变化是什么?】
    字与字之间的实际大小并不一致,即使是唐楷也不一致。
    唐楷的一般规律是笔画少字写小写重,笔画多的字写大写轻,这样就能保证视觉上的美观。当然,这里的小与大只是相对而言,非常细微的变化。
    在行书上并没有这样明显的规律,但是轻重大小也要根据帖来表现出来。
    【字与字之间的间距排布是怎样的?】
    【如果是行书的话,字与字之间的连带关系是怎样的?】
    【字与字之间的墨色浓淡枯湿变化是怎样的?】
    比如很多行书帖中可以很明显地看出来何处沾墨何处枯笔,那么在通临的时候这些也是需要注意的。
    四、其他与碑帖特点相关的内容
    【字形是什么?多为长形?正方形?还是扁形?】
    【笔画向背的势是怎样的?】
    【字帖整体的墨色浓淡情况?】

     为什么你连简单的字都写不好
     2017-03-21
    在看一个字的时候除了看到这个字是由什么笔画组成的,还要去关注这些笔画的位置或者起收笔之间存在怎样的关系,这样才能把字写好。
     2017-03-21
    一、收笔要放在同一水平线上
    不论是撇+竖弯钩、撇+捺、撇+点、撇+撇点还是撇+撇+捺,只要是由两只脚或三只脚支撑的字,都要把它们落在同一个水平线上,不能一只脚翘起来。
     2017-03-21
    二、除了斜钩,其他笔画要同时落在一个水平线
    这一条其实是上一条的延伸。在这种字形里,要把左边笔画+右边的撇的落点统一到一个平面,这样整个字会看起来很清爽,否则下面会显得参差不齐。
     2017-03-21
    三、同向笔画之间距离要均等
    笔画分横向和纵向,在一个字里经常会出现同一个方向的笔画有很多的情况,比如下面两个字就是横向笔画比较多:这些横之间都是有距离的,要把每一横之间的距离写均匀:
     2017-03-21
    四、每个字里只有一笔最长,其他笔画处理成相近长度
    下面两个字都是有很多横的:
    要把最长的一横拉开,同时其他横的长度尽量处理得差不多,否则会乱:
     2017-03-21
    五、很多撇平行在一起时,收笔边缘要处理成弧线
    经常会碰到这种情况,好多撇并排:在这种情况下,中间两撇写长,收笔与上面的撇、下面的钩形成一个弧线,避免忽长忽短、参差不齐:
     2017-03-21
    六、四点底找准方向
    首先,在写四点底的时候要写成这样——起笔对齐,中间的两点垂直,左边点朝右,右边点朝左:
    然后在写四点底组成的字的时候,要用这四个点去找上半部分相对应的点,比如「照」:
    再比如「然」:
     2017-03-21
    七、心字底找准方向
    先把「心」字写成这样——中间一点的收笔与其他三笔的起笔在一条线:当「心」字作为偏旁时,把中间一点压低,然后每一笔都要和上面的部分找到相对应的笔画:
    这样子才不会跑偏。
     2017-03-21
    八、走字底要做到「两个对齐」
    以「远」为例,先把里边的部分写好:
    然后,左边走字底的撇,要和右边「元」的下面对齐,成一条水平线:
    最后一笔,平捺——先写一条长斜线到「元」的右边缘线的位置,然后转折出尖,不能早不能晚:
     2017-03-21
    九、当上面是左右结构时,下面的独体字要与上面相呼应
    有一种上下结构是「a+a+B」的组合方式,比如「替」。在写的时候先把上面两个「夫」写好——相等、对齐:然后,在写下面的「日」的时候,要注意两个竖应和上面的两个「夫」的中轴线对齐:还有一种「a+b+C」的上下结构也是一样,比如「碧」。
    先把上面写好:下面的「石」,第一横和两个竖要和上面两部分的中轴线相对:

     不同结构的字怎么写才好看
     2017-03-21
    对于所有的字,都让这些字体轻轻向右上方扬起一些,看上去会很好看。
     2017-03-21
    上下结构:
    精髓在于:发现字体的主要部分,并凸显出来。
    左右结构:
    精髓在于:通常偏旁部首应该写的小一些。
    半包围结构:
    精髓在于:包在里面的部分才是重点,一定要写在恰当的位置上。
    全包围结构:
    精髓在于:留足够的空间写里面的东西。
     2017-03-21
    较长的字:
    精髓在于:尽量把字体上面的那一部分写的小一些,这样可以有空间写下面的部分。而且还要写得宽一些,不能一味的瘦高。
    较宽的字:精髓在于:尽量写得高一些,避免矮胖矮胖的情况发生。
    较稀疏的字:
    精髓在于:要承认一口气把每个字都写好是很难的。

     字外功夫:如何提高指力和笔力
     2017-03-21
    心力眼力(这个非常重要,但并非常规训练,需要结合笔力指力综合训练):
    心力眼力其实不是力,直白说就是如何对力道和运笔胸有成竹,知道用多大的力和怎样用力才能带来快感。
    两者的区别在于,心力,是指落笔之前的单字、通篇规划能力,即在写字之前,要对单个字乃至通篇大体有数;眼力,是指习作完成后的自我总结能力,即在写字之后,能准确地观察出作品的问题,哪里力度掌握不好,哪里结构松散等等。
    感觉这玩意儿真不好练。笼统地讲,所有的书法练习都和心力眼力有关,但又不会直接产生作用。相对来讲,最有针对性的训练方法也是最没创意的方法——读古帖。
     2017-03-21
    ·零散时间的笔法训练:虚实线、曲线线条练习
    注意三个要点:轻重、粗细、露锋藏锋(就是下笔收笔带不带尖)。顺序都是左到右和上到下,且以从左到右优先。
     2017-03-21
    ·双勾法和描摹练习
    这个是重点中的重点。
    双勾法:先外勾,以描摹的方式,把例字的外框描出来,再以内勾的方式将框内填满。硬笔训练中,内勾可以用铅笔按照例字的笔法以单个线条的方式在框内写出来,不需要填满;双勾完毕之后再在旁边临写一遍。
     2017-03-21
    另外,描摹练习不光可以使用毛笔古帖,也可以使用白描图谱。这对于手指灵敏度、准确度以及线条的粗细力度变化有极好的帮助。
     2017-03-21
    ·笔画训练
    硬笔书法28种基本笔画,一定要花大块的时间用毛笔古帖来练习。零散的时间,则可以进行笔画训练和上面讲到的虚实线训练。
     2017-03-21
    三、最后,如果给读帖、描摹、临帖编制一个训练方法,或者是练习的流程,大体应该是这样的:
    (1)基本功训练(上文的所有内容)(2)读帖(仔细观察作者的笔法、结构和章法,在脑海中形成印象,训练心力)
    (3)描摹(可采用双勾法,训练笔法)
    (4)对临(手眼结合,训练笔力和眼力)
    (5)对照(把自己写的和帖子相对照,找到不同的地方,找到失误的地方,训练眼力)
    (6)背临(训练心力)
    (7)对照(把自己写的和帖子相对照,找到不同的地方,找到失误的地方,训练眼力)
    (8)练习相近字(根据对照后找到的弱点进行针对性训练,比如,「喜」这个字的长横写不好,练过之后再继续练习「善」「普」「姜」等带有长横的字。比如,「册」几个纵向笔画之间的间距掌握不好,那就练完之后再继续练习「川」「卅」「珊」「助」等字)
    (9)篇章练习

     慢慢写是最快的捷径
     2017-03-21
    每个字都要花时间,只有这样才能把基本功打扎实。步入书法的门槛,一定要以谦虚敬畏之心对待,才能把你的书法审美能力提高。每个人的书写习惯不一样,所以很多人觉得没必要临成一模一样的。但在不断揣摩的过程中,你才能分清你那些书写习惯有没有改的必要,硬笔也是有笔法的。
     2017-03-21
    慢慢写是最快的捷径。
    拿纸蒙在字帖上写个四五遍并不代表你就懂了每个字的结构,我至今也没练完一本字帖,还在学习中。但毫无疑问的是,不走心的摹帖所做的都是无用功,唯一让你安心的是你觉得自己已经付出了时间足够认真了。
    共勉。

     

    展开全文
  • 在本教程中,我们将逐步介绍如何使用MNIST 数据集构建手写数字分类器。 对于深度学习新手来说,这个练习可以说是和“Hello World”等同的。MNIST 是广泛使用的用于手写数字分类任务的数据集。它由70,000个有标记的,...

    文档英文原版参见Handwritten Digit Recognition

    在本教程中,我们将逐步介绍如何使用MNIST 数据集构建手写数字分类器。 对于深度学习新手来说,这个练习可以说是和“Hello World”等同的。

    MNIST 是广泛使用的用于手写数字分类任务的数据集。它由70,000个有标记的,28x28分辨率的手写数字图像组成。数据集分为6万个训练图像和10,000个测试图像。共有10个类别(10个数字每个一类)。目前的任务是使用60,000个训练图像训练模型,并随后在10,000张测试图像上测试其分类准确率。
    图1:MNIST 数据集的样本

    图1:MNIST 数据集的样本

    前提条件

    为了完成以下教程,我们需要:

    pip install jupyter requests

    加载数据

    在我们定义模型之前,我们先来获取MNIST 数据集。

    以下源代码下载并将图像和相应的标签加载到内存中。

    import mxnet as mx
    mnist = mx.test_utils.get_mnist()

    运行上述源代码后,整个MNIST数据集应该被完全加载到内存中。 请注意,对于大型数据集,先预加载整个数据集是不可行的。我们需要一种可以直接从源头快速有效地传输数据的机制。 MXNet数据迭代器提供了解决方案。 数据迭代器是一种将数据输入到MXNet训练算法中,能够非常简单的初始化和使用,并针对速度进行了优化的机制。 在训练时,我们通常以小批量处理训练样本,并且在整个训练生命周期中将多次处理每个训练样本。 在本教程中,我们将配置数据迭代器以100进行批量提供样本。请记住,每个样本都是一个28x28的灰度图像及其对应的标签。

    图像批次通常由4维数组表示,其形状为(batch_size,num_channels,width,height)。 对于MNIST数据集,由于图像是灰度级,所以只有一个颜色通道。 此外,图像是28×28像素,因此每个图像的宽度和高度等于28。因此,输入的形状为(batch_size,1,28,28)。 另一个重要的考虑因素是输入样本的顺序。 当输入训练样本时,关键是我们不会连续地给相同标签的样本。 这样做会减缓训练。 数据迭代器通过随机混洗输入数据来处理这个问题。 请注意,我们只需要打乱训练数据的顺序,与测试数据无关。

    以下源代码初始化MNIST数据集的数据迭代器。 请注意,我们初始化两个迭代器:一个用于训练数据,一个用于测试数据。

    batch_size = 100
    train_iter = mx.io.NDArrayIter(mnist['train_data'], mnist['train_label'], batch_size, shuffle=True)
    val_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
    

    训练

    我们将介绍实现手写数字识别任务的几种方法。 第一种方法利用传统的称为多层感知(MLP)的深度神经网络结构。 我们将讨论其缺点,并引入第二种更先进的方法,称为卷积神经网络(CNN),它已被证明在图像分类任务中工作得很好。

    多层感知

    第一种方法利用多层感知器来解决这个问题。 我们将使用MXNet的符号交互定义MLP。 我们首先为输入数据创建一个占位符变量。 当使用MLP时,我们需要将我们的28x28图像平展为784(28 * 28)原始像素值的平面1维结构。 只要我们在所有图像中都这样做,平面向量中像素值的顺序就不重要了。

    data = mx.sym.var('data')
    # Flatten the data from 4-D shape into 2-D (batch_size, num_channel*width*height)
    data = mx.sym.flatten(data=data)

    人们可能会想知道平展是否会丢失有价值的信息。 这确实是真的,当我们谈论保持输入形状不变的卷积神经网络时,我们将会更多地讨论这个问题。现在,我们将继续使用平展的图像。

    MLP包含几个全连接层。 全连接层(简称FC层)中每个神经元都与前一层中的每个神经元相连。 从线性代数的角度来说,FC层对n×m的输入矩阵X做仿射变换,并且输出大小为n×k的矩阵Y,其中k是FC层中的神经元的个数。 k也被称为隐藏层大小。根据等式Y=WX+b 计算输出Y。 FC层具有两个可学习的参数,即m×k的权重矩阵W和m×1的偏置向量b。

    在MLP中,大多数FC层的输出被送到激活函数中,该函数进行非线性操作。 这一步是至关重要的,它使神经网络能够对线性不可分的输入进行分类。 常用的激活函数是 sigmoid,tanh和ReLU。 在这个例子中,我们将使用具有多个所需属性的ReLU激活函数,这也是通常的默认选择。

    以下代码定义两个全连接层,每个层分别为128个和64个神经元。此外,这些FC层夹ReLU激活层之间。

    # The first fully-connected layer and the corresponding activation function
    fc1  = mx.sym.FullyConnected(data=data, num_hidden=128)
    act1 = mx.sym.Activation(data=fc1, act_type="relu")
    
    # The second fully-connected layer and the corresponding activation function
    fc2  = mx.sym.FullyConnected(data=act1, num_hidden = 64)
    act2 = mx.sym.Activation(data=fc2, act_type="relu")

    最后一个全连接层的隐藏大小通常等于数据集中的输出类别个数。该层的激活功能将是softmax函数。 Softmax层将其输入映射到每一类输出的概率分数。 在训练阶段,损失函数计算网络预测的概率分布(softmax输出)与标签给出的真实概率分布之间的交叉熵。

    以下源代码定义了大小为10的最后一层全连接层。10代表的是数字的个数。该层的输出被送到SoftMaxOutput 层,其一次性就算softmax和交叉熵 loss。 请注意,仅在训练时才计算损失函数。

    # MNIST has 10 classes
    fc3  = mx.sym.FullyConnected(data=act2, num_hidden=10)
    # Softmax with cross entropy loss
    mlp  = mx.sym.SoftmaxOutput(data=fc3, name='softmax')

    图2:MNIST的MLP网络结构

    图2:MNIST的MLP网络结构

    定义了数据迭代器和神经网络之后,我们可以开始训练。这里我们将使用MXNet中的module 功能,它提供了一个用于在预定义网络上训练和推理的高层抽象。module API允许用户指定适当的参数来控制训练的进行。

    以下代码初始化一个模块来训练我们上面定义的MLP网络。对于训练,我们将利用随机梯度下降(SGD)优化器。更进一步说,我们将使用小批量SGD。标准SGD每次训练一个样本,在实际中是非常慢的,可以通过小批量处理样本来加速这个过程。因此,我们将选择一个合理的批次,这里是100。要选择的另一个参数是学习率,它控制优化器求解所需的步长。我们也将选择一个合理的值,这里是0.1。设置的批量大小和学习率通常被称为超参数。他们的值将对训练产生很大的影响。为了教学,我们将直接从一些合理和安全的值开始。在其他教程中,我们将讨论如何找到超参数的组合以获得最佳的模型性能。

    通常,直到收敛才停止训练,这意味着我们从训练数据中学到了一套好的模型参数(权重+偏差)。 为了教学,我们将训练10个批次。 一个批次是遍历整个训练数据集。

    import logging
    logging.getLogger().setLevel(logging.DEBUG)  # logging to stdout
    # create a trainable module on CPU
    mlp_model = mx.mod.Module(symbol=mlp, context=mx.cpu())
    mlp_model.fit(train_iter,  # train data
                  eval_data=val_iter,  # validation data
                  optimizer='sgd',  # use SGD to train
                  optimizer_params={'learning_rate':0.1},  # use fixed learning rate
                  eval_metric='acc',  # report accuracy during training
                  batch_end_callback = mx.callback.Speedometer(batch_size, 100), # output progress for each 100 data batches
                  num_epoch=10)  # train for at most 10 dataset passes

    预测

    训练完成后,我们可以通过在测试数据上预测来评估训练好的模型。 以下代码对每张测试图像计算预测概率得分。 prob[i][j]是第i个测试图像的第j个输出类的概率。

    test_iter = mx.io.NDArrayIter(mnist['test_data'], None, batch_size)
    prob = mlp_model.predict(test_iter)
    assert prob.shape == (10000, 10)

    由于数据集对所有测试图像也有标签,因此可以按以下方式计算准确率度量:

    test_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
    # predict accuracy of mlp
    acc = mx.metric.Accuracy()
    mlp_model.score(test_iter, acc)
    print(acc)
    assert acc.get()[1] > 0.96

    如果一切顺利,我们应该看到准确率大约为0.96,这意味着我们可以准确预测96%的测试图像中的数字。这是一个很好的结果。但是正如我们将在本教程的下一部分中看到的那样,我们可以做的比这更好。

    卷积神经网络

    首先,我们简要介绍了MLP的一个缺点,我们之前说需要舍弃输入图像的原始形状并将其平展为一个向量,然后才能将其作为MLP第一个全连接层的输入。实际上这是一个重要的问题,因为我们没有利用到沿水平轴和垂直轴方向,图像中的像素所具有的天然空间相关性。卷积神经网络(CNN)旨在通过使用更结构化的权重表示来解决这个问题。 它没有平展图像并进行简单的矩阵乘法,而是采用一个或多个卷积层,每个卷积层在输入图像上做2维卷积运算。

    单个卷积层包括一个或多个滤波器,每个滤波器都起到特征检测器的作用。在训练时,CNN学习适用于这些滤波器的合理的表示(参数)。与MLP类似,卷积层的输出进行非线性变换。除了卷积层之外,CNN的另一个关键方面是池化层。 池化层用于使CNN保持转平移不变性:一个数字即使向左/向右/向上/向下移动几个像素,它仍然保持不变。池化层将n×m的区域降维成单个值,以降低网络对空间位置的敏感性。CNN中每个卷积层(+激活层)之后始终有池化层。

    以下代码定义了称为LeNet的卷积神经网络架构。LeNet是一个众所周知的,在数字分类任务上表现不错的网络。我们将使用与原始LeNet实现略有不同的版本,用tanh激活代替 sigmoid激活。

    data = mx.sym.var('data')
    # first conv layer
    conv1 = mx.sym.Convolution(data=data, kernel=(5,5), num_filter=20)
    tanh1 = mx.sym.Activation(data=conv1, act_type="tanh")
    pool1 = mx.sym.Pooling(data=tanh1, pool_type="max", kernel=(2,2), stride=(2,2))
    # second conv layer
    conv2 = mx.sym.Convolution(data=pool1, kernel=(5,5), num_filter=50)
    tanh2 = mx.sym.Activation(data=conv2, act_type="tanh")
    pool2 = mx.sym.Pooling(data=tanh2, pool_type="max", kernel=(2,2), stride=(2,2))
    # first fullc layer
    flatten = mx.sym.flatten(data=pool2)
    fc1 = mx.symbol.FullyConnected(data=flatten, num_hidden=500)
    tanh3 = mx.sym.Activation(data=fc1, act_type="tanh")
    # second fullc
    fc2 = mx.sym.FullyConnected(data=tanh3, num_hidden=10)
    # softmax loss
    lenet = mx.sym.SoftmaxOutput(data=fc2, name='softmax')

    图3:LeNet的第一层卷积和池化

    图3:LeNet的第一层卷积和池化

    现在我们用和之前相同的超参数训练LeNet。请注意,如果可以用GPU,我们建议使用它,因为LeNet比以前的多层感知器更复杂,计算更多,GPU可以大大加快了计算。为此,我们只需要将mx.cpu() 更改为mx.gpu(),其余操作将由MXNet完成。和之前一样,我们将在十个批次后停止训练。

    # create a trainable module on GPU 0
    lenet_model = mx.mod.Module(symbol=lenet, context=mx.cpu())
    # train with the same
    lenet_model.fit(train_iter,
                    eval_data=val_iter,
                    optimizer='sgd',
                    optimizer_params={'learning_rate':0.1},
                    eval_metric='acc',
                    batch_end_callback = mx.callback.Speedometer(batch_size, 100),
                    num_epoch=10)
    

    预测

    最后,我们将使用经过训练的LeNet模型来生成测试数据的预测。

    test_iter = mx.io.NDArrayIter(mnist['test_data'], None, batch_size)
    prob = lenet_model.predict(test_iter)
    test_iter = mx.io.NDArrayIter(mnist['test_data'], mnist['test_label'], batch_size)
    # predict accuracy for lenet
    acc = mx.metric.Accuracy()
    lenet_model.score(test_iter, acc)
    print(acc)
    assert acc.get()[1] > 0.98

    如果一切顺利,我们应该看到使用LeNet进行预测的准确率更高。 使用CNN,我们应该能够在测试图像上达到98%的准确率。

    总结

    在本教程中,我们已经学会了如何使用MXNet来解决标准的计算机视觉问题:手写数字图像分类。 你已经看到如何使用MXNet快速轻松地构建,训练和评估MLP和CNN等模型。

    展开全文
  • 使用Tensorflow和MNIST识别自己手写的数字

    万次阅读 多人点赞 2017-03-27 06:17:32
    最近在学习神经网络相关的东西,发现有很多资料是Tensorflow教程上的内容,但是教程很多只是一个验证官方程序的过程。如何将官方程序变成自己可以利用的程序,网上似乎资料比较少,所以我就来介绍一下如何使用...
  • GitHub教程 Git Bash详细教程

    万次阅读 多人点赞 2018-01-17 16:12:58
    -`ω´-)所以我决定自己一边摸索一边记录,写教程造福那些理解力跟我一样差的人…… 第一篇教程会涉及如下内容(按照一般人的使用流程): 下载、登录Git Bash 如何在Git Bash中进入或者退出文件夹 ...
  • ❤️五万字《C语言动漫教程》❤️

    万次阅读 多人点赞 2021-07-01 13:44:49
    六万字最全C语言动漫式教程,本文连载直到卡死
  • pycharm 教程(二)设置字体大小

    千次阅读 2017-08-14 17:04:46
    pycharm 是很的一个IDE,在 windows 和 mac 下,都能很的运行。唯一缺点是启动慢。 默认字体太小,在 mac 下,需要瞪大24K氪金狗眼才能看清。 为了保护眼睛,我们需要把字体调整大一些: 步骤: Settings–...
  • 最合适代码的字体

    千次阅读 2015-01-27 15:05:12
    最合适代码的字体 每天盯着屏幕代码,自然需要寻找一种看得舒服的字体,能让自己的代码赏心悦目,一般来说,我们选择用于显示代码的字体,有如下几个要求: 字母的宽度一致 或称为等宽字体...
  • 13 万字C语言保姆级教程,从入门到精通。
  • 硬肝4.4w为你成Java开发手册

    万次阅读 多人点赞 2020-05-09 09:29:28
    先来看一下本篇文章的思维导图吧,我会围绕下面这些内容进行...编程语言还比较理解,那么什么是 计算平台 呢? 计算平台是在电脑中运行应用程序(软件)的环境,包括硬件环境和软件环境。一般系统平台包括一台电脑
  • Develop:是我们需要存放代码的文件夹,在这里面,存放的是我们写好的需要进行版本控制的代码文件 Git:这是我们Git软件的安装文件夹,为了图方便,放在这里了 Repository:英文是仓库的意思。这个文件夹是...
  • 【钢琴入门自写教程 1】小星星弹奏

    千次阅读 2019-03-09 05:20:35
    1. 熟悉键位 刚接触钢琴,给我的第一感觉时,这手不是我的,太僵了!!! 和我用计算机键盘打字的手,感觉完全是两...小组 小一组 小二组 小三组 小四组 C 6 、7 1-2-3-4-5-6-7 1-2-3-4-5-6-7 1-2-3-4...
  • IDEA(jetbrain通用)优雅级使用教程

    万次阅读 多人点赞 2016-12-01 17:59:37
    前面过一篇IDEA的入门级文章,但是只学会了那些配置啊什么的并不能提高我们的开发效率。事实上,如果你IDEA用的足够熟练,就可以把鼠标扔了。 附入门级教程传送门: ...
  • 【Pycharm教程】(二)设置字体大小

    千次阅读 2017-01-21 12:45:43
    pycharm 是很的一个IDE,在windows下,和macOS下,都能很的运行。...默认字体太小,在mac下,需要瞪大24K氪金狗眼才能看清。 为了保护眼睛,我们需要把字体调整大一些:  步骤: Settings-->Edi
  • 区块链爱好者(QQ:53016353) ...为了让更多人认识了解加入P2POOL矿池算力网络,我在这里一个详细全面的教程。 以当前难度510929738为例,500T算力1个小时就能算完,也就是挖到一个块=2
  • 这句的意思便是:当按键按下时,运行一个我们写好的程序部分(事件),名为keyDown 既然我们可以检测键盘是否按下了,我们就要一个keyDown程序部分响应它啦~ 要在和之间上   你看到这个结构头都大...
  • 给大数据开发初学者的话 | 附教程

    千次阅读 多人点赞 2017-11-27 17:02:20
    实际环境中一般自己较少编写程序使用API来数据到HDFS,通常都是使用其他框架封装的方法。比如:Hive中的INSERT语句,Spark中的saveAsTextfile等。 建议了解原理,会Demo。 3.3 Sqoop Sqoop是一个主要...
  • 苹果MAC电脑双系统教程——MAC安装Windows双系统教程

    万次阅读 多人点赞 2020-12-17 23:50:29
    在前面: Mac用户在使用过程中,多少有的时候还会使用Windows,想装一个咋办?找某宝需要花钱150元,看这篇文章,十几分钟看明白,自己动手丰衣足食!所以本文将以图文教程教你如何安装双系统,开机随意选择,想...
  • OpenGL入门教程

    万次阅读 多人点赞 2018-12-03 18:14:53
    WebGL(全Web Graphics Library)是一种3D绘图协议,这种绘图技术标准允许把JavaScript和OpenGL ES 结合在一起,通过增加OpenGL ES 的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,这样Web...
  • 学习Verilog HDL有一些时间,大概一年前的的这个时候开始的吧,从一点都不懂开始学,主要还是看夏宇闻老师的这本书入的门——《Verilog数字系统设计教程》,书写的特别。然后就是看网上的一些资料,学习语言,学习...
  • Latex:入门教程

    万次阅读 多人点赞 2017-01-16 14:40:51
    http://blog.csdn.net/pipisorry/article/details/54571521总的来说,LaTex是一套排版系统,...简单来说,你只要按照要求撰写tex文件,就能够通过LaTex生成排版的pdf文件。有些人可能听到程序就头大了,其实使用...
  • 微信表情包制作教程 纯纯的干货,目的在于教会我麻麻制作自己的表情包 软件安装 网上有很多教程,不同的人用的软件不同,我这里只用了两个软件 对于找软件安装包的问题,推荐一个很...作为一款非常的矢量图...
  • C++教程(最全)

    万次阅读 多人点赞 2020-03-04 15:21:05
    C++ 是区分大小的编程语言。 4 数据类型 4.1基本数据类型 七种基本的C++数据类型:bool、char、int、float、double、void、wchar_t 类型修饰符:signed、unsigned、short、long 注:一些基本类型可以使用一个或多...
  • 也是拖,拖到Dock以外的任何地方,图标会变成半透明,并出现“移除”两个,这就说明你正在删除它,松开鼠标就了。放心,删除的只是快捷方式,原程序还在,只是Dock上没了,想放上去的时候按上面的方法就可以再次...
  • Android快速转战Kotlin教程

    万次阅读 多人点赞 2019-02-16 09:24:57
    了,到这里我们基本掌握了Kotlin在安卓中的应用,那么接下来就需要去学习一下kotlin设计模式以及一些进阶知识~ 进阶 一、Kotlin设计模式 本文只列出几个常用的设计模式 1、观察者模式( observer pattern ...
  • 第一章: 利用神经网络识别手写数字

    万次阅读 2017-03-28 11:35:51
    当然,如果本章只是一个识别手写数字的计算机程序,那么将非常短小!在编写过程中,我们将介绍很多神经网络的关键思想,包括人工神经网络的两大类别(感知器和sigmoid神经元)以及神经网络标准学习算法,即随机...
  • MySQL系列教程(一)

    万次阅读 多人点赞 2016-09-19 09:55:35
    但是消息是这次的上层领导们终于开始认识到互联网的重要性,理解了安全可控对于一个国家的重要性,而互联网公司的成熟经验具有很的借鉴意义。 总结 MySQL数据库早已不是原来的迷你数据库,其在功能性与性能方面...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 83,000
精华内容 33,200
关键字:

怎么才能写好字教程