分类: iOS开发 Swift开发 2014-09-12 00:52 3549人阅读 评论(8) 收藏 举报 html
SwiftiOS8Core Image滤镜CIFiltergit
iOS8 Core Image In Swift:自动改善图像以及内置滤镜的使用xcode
iOS8 Core Image In Swift:更复杂的滤镜
app
iOS8 Core Image In Swift:人脸检测以及马赛克dom
iOS8 Core Image In Swift:视频实时滤镜
ide
上 面那篇文章主要是Core Image的基础,只是为了说明CIImage、CIFilter、CIContext,以及基础滤镜的简单使用。在上一篇中几乎没有对滤镜进行更复杂的 操做,都是直接把inputImage扔给CIFilter而已,而Core Image实际上还能对滤镜进行更加细粒度的控制,咱们在新的工程中对其进行探索。为此,我从新创建了一个空的workspace,并把以前所使用的工程 添加到这个workspace中,编译、运行,没问题的话咱们就开始建立新的工程。post
经过workspace左下角的Add Files to添加已有的工程文件(xx.xcodeproj):测试
当添加工程到workspace的时候,记得要把被添加的工程关掉,否则workspacce不能识别。 另外,在流程上这篇也会与上一篇不一样,上一篇一开始我就给出了代码,而后先看效果再步步为营,这篇不会在一开始给出代码。 |
用Single View Application的工程模板创建一个新的工程,在View上放一个UIImageView,仍是一样的frame,一样的ContentMode设置为Aspect Fit,一样的关闭Auto Layout以及Size Classes,最后把上个工程中使用的图片复制过来,在这个工程中一样使用这张图。
网站
作完上面这些基础工做后,咱们回到VC中,把showFiltersInConsole方法从上个工程中复制过来,而后在viewDidLoad里调用,在运行以前咱们先看看Core Image有哪些类别,毕竟所有的滤镜有127种,不可能一一用到的。
类别有不少,并且咱们从上一篇中知道了滤镜能够同时属于不一样的类别,除此以外,类别还分为两大类:
kCICategoryDistortionEffect 扭曲效果,好比bump、旋转、hole
kCICategoryGeometryAdjustment 几何开着调整,好比仿射变换、平切、透视转换
kCICategoryCompositeOperation 合并,好比源覆盖(source over)、最小化、源在顶(source atop)、色彩混合模式
kCICategoryHalftoneEffect Halftone效果,好比screen、line screen、hatched
kCICategoryColorAdjustment 色彩调整,好比伽马调整、白点调整、曝光
kCICategoryColorEffect 色彩效果,好比色调调整、posterize
kCICategoryTransition 图像间转换,好比dissolve、disintegrate with mask、swipe
kCICategoryTileEffect 瓦片效果,好比parallelogram、triangle
kCICategoryGenerator 图像生成器,好比stripes、constant color、checkerboard
kCICategoryGradient 渐变,好比轴向渐变、仿射渐变、高斯渐变
kCICategoryStylize 风格化,好比像素化、水晶化
kCICategorySharpen 锐化、发光
kCICategoryBlur 模糊,好比高斯模糊、焦点模糊、运动模糊
kCICategoryStillImage 能用于静态图像
kCICategoryVideo 能用于视频
kCICategoryInterlaced 能用于交错图像
kCICategoryNonSquarePixels 能用于非矩形像素
kCICategoryHighDynamicRange 能用于HDR
这些专业词太难翻译了,有不许确的地方还望告知 |
此外还有咱们以前用到的kCICategoryBuiltIn。
咱们把kCICategoryColorAdjustment这个类别下的滤镜打印出来看看:
有11个滤镜,其中有一个CIHueAdjust,这个看名字应该是修改图像色调的,效果应该会比较明显,看看它有哪些参数:
它的详细信息里除了咱们以前了解的inputImage和所属分类信息之外,多了个inputAngle,显然这是一个输入参数,并且这个参数也打印的很是清晰,其中包括了:
参数类型:NSNumber
默认值:0
kCIAttributeIdentity:虽然这个值大部分状况下与默认值是同样的,可是它们的含义不同,kCIAttributeIdentity表示的含义是这个值被应用到参数上的时候,就表示被应用的参数不会对inputImage形成任何影响
最大值:Ԉ
最小值:-Ԉ
属性类型:角度
上面的这些参数以及取值对不一样的CIFilter来讲都不同,要具体状况具体分析。
了解了以上状况后,咱们就能够开始编码了。首先在VC里添加上个工程中的经常使用属性:
class ViewController: UIViewController {
@IBOutlet var imageView: UIImageView!
@IBOutlet var slider: UISlider!
lazy var originalImage: UIImage = {
return UIImage(named: "Image")
}()
lazy var context: CIContext = {
return CIContext(options: nil)
}()
var filter: CIFilter!
......
与以前工程中不一样的是,我多加了一个UISlider,Main.storyboard中VC的view像这样:
把UIImageView及UISlider的连线与VC中的链接起来,而后咱们在viewDidLoad方法里写上:
override func viewDidLoad() {
super.viewDidLoad()
imageView.layer.shadowOpacity = 0.8
imageView.layer.shadowColor = UIColor.blackColor().CGColor
imageView.layer.shadowOffset = CGSize(width: 1, height: 1)
slider.maximumValue = Float(M_PI)
slider.minimumValue = Float(-M_PI)
slider.value = 0
slider.addTarget(self, action: "valueChanged", forControlEvents: UIControlEvents.ValueChanged)
let inputImage = CIImage(image: originalImage)
filter = CIFilter(name: "CIHueAdjust")
filter.setValue(inputImage, forKey: kCIInputImageKey)
slider.sendActionsForControlEvents(UIControlEvents.ValueChanged)
showFiltersInConsole()
}
imageView的设置同之前同样,增长点阴影显得好看多了。
接着对slider初始化,在以前咱们了解到CIHueAdjust滤镜的inputAngle参数最大值是Ԉ,最小值是负Ԉ,默认值是0,就用这些值来初始化,而后添加一个当值发生改变时触发的事件。
初始化filter,因为只有一个滤镜,filter对象也能够重用,设置完inputImage后,触发slider的事件就能够了。
valueChanged方法实现:
@IBAction func valueChanged() {
filter.setValue(slider.value, forKey: kCIInputAngleKey)
let outputImage = filter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
filter会在每次触发这个事件的时候更新inputAngle属性,同时输出到imageView上。
虽然我并非在Storyboard里把slider的valueChanged事件链接到VC的方法上,可是在这里使用@IBAction 也是适当的,这样能够代表这个方法不是业务逻辑方法,而是一个UI控件触发的方法。
编译、运行,应该能够看到效果了。
在此以前,不管是使用简单滤镜,仍是能动态修改参数值的滤镜,都不算复杂,由于咱们最多也只是对一个滤镜设置点参数而已。但是若是现有的滤镜没有想要的效果,或者说单个滤镜实现不了本身想要的效果,就只能本身处理了,其中,最简单的作法是把多个滤镜组合起来。
Core Image并无内置相似于老电影的效果,就是那种影像有点发黄,同时还会带点黑条、白条之类的,而咱们若是要实现这种效果,整体上就像这样:
大体过程以下:
须要使用CISepiaTone滤镜,CISepiaTone能使总体颜色偏棕褐色,又有点像复古
须要建立随机噪点图,很像之前电视机没信号时显示的图像,再经过它生成一张白斑图滤镜
须要建立另外一个随机噪点图,而后经过它生成一张黑色磨砂图滤镜,就像是一张使用过的黑色砂纸同样
把它们组合起来
在开始以前首先要知道一件事,咱们已经知道了一些简单的滤镜,它们只须要设置inputImage就好了;还有一些除了inputImage参数外有其余参数的滤镜,除此以外,还有一些滤镜不须要任何参数,就是上面提到的随机噪点图,另外,有些Core Image滤镜会生成无限大小的图,好比CICategoryTileEffect类别下的滤镜,在渲染它们生成的图以前,必须先把这些无限大小的图裁剪一番,你能够经过CICrop滤镜来完成这一步,也能够经过在一个有限的矩形范围之类渲染这张图来达到一样的效果。
而后咱们就动手吧。
在 VC里添加一个IBAction方法:oldFilmEffect,而后在Storyboard的VC上增长一个按钮,就叫“老电影”,而后链接到 oldFilmEffect方法上,oldFilmEffect方法实现的代码稍后给出,这里先描述下详细步骤,其实经过这些详细步骤,已经能够本身先实 现出来了:
设置inputImage为原图
设置inputIntensity为1.0
用CIRandomGenerator生成随机噪点滤镜,而后经过imageByCroppingToRect方法对其进行裁剪,在imageByCroppingToRect方法内Core Image隐式的使用了CICrop滤镜。
接下来使用CIColorMatrix滤镜,该滤镜能够很方便的调整图片中RGBA各份量的值,其参数设置以下:
设置inputImage为CIRandomGenerator生成的随机噪点图
设置inputRVector、inputGVector和inputBVector为(0,1,0,0)
设置inputBiasVector为(0,0,0,0)
用CISourceOverCompositing(源覆盖)滤镜把前景图(inputImage)覆盖在背景图(inputBackgroundImage)上:
设置inputImage为CISepiaTone滤镜生成的图
设置inputBackgroundImage为白斑图滤镜
仍是先用CIRandomGenerator生成随机噪点图,而后用CIAffineTransform滤镜对其进行处理,其实就是把生成的点放大。参数设置以下:
设置inputImage为CIRandomGenerator生成的随机噪点图
设置inputTransform为x放大1.5倍、y放大25倍,把点拉长、拉厚,可是它们仍然是有颜色的
在这里除了使用CIAffineTransform滤镜外,还有一种替代方法能够达到一样的效果,同时不用显式建立CIAffineTransform滤镜,就是使用CIImage的imageByApplyingTransform:方法。
再次用CIColorMatrix滤镜对颜色进行处理:
设置inputImage为CIAffineTransform生成的图
设置inputRVector为(4,0,0,0)
设置inputGVector、inputBVector和inputAVector为(0,0,0,0)
设置inputBiasVector为(0,1,1,1)
如今产生的是一个蓝绿色磨砂图滤镜,再把CIMinimumComponent滤镜应用到这个蓝绿色磨砂图滤镜产生的图上。CIMinimumComponent滤镜会使用r、g、b的最小值生成一张灰度图像。
使用CIMultiplyCompositing作最后的组合,参数设置以下:
设置inputImage为CISourceOverCompositing滤镜生成的图(内含CISepiaTone、白斑图滤镜的效果)
设置inputBackgroundImage为CIMinimumComponent滤镜生成的图(内含黑色磨砂图滤镜效果)
最后把CIMultiplyCompositing生成出的图输出到imageView上,仍是之前的方式,先转成CGImage,再把CGImage转成UIImage。
有点小长,并且同时用到了多个滤镜,其实想表达的意思并无那么复杂,可使用kCICategoryBuiltIn把全部的滤镜打印出来,而后对照着看它们的参数。
这里是oldFilmEffect方法实现:
@IBAction func oldFilmEffect() {
let inputImage = CIImage(image: originalImage)
// 1.建立CISepiaTone滤镜
let sepiaToneFilter = CIFilter(name: "CISepiaTone")
sepiaToneFilter.setValue(inputImage, forKey: kCIInputImageKey)
sepiaToneFilter.setValue(1, forKey: kCIInputIntensityKey)
// 2.建立白斑图滤镜
let whiteSpecksFilter = CIFilter(name: "CIColorMatrix")
whiteSpecksFilter.setValue(CIFilter(name: "CIRandomGenerator").outputImage.imageByCroppingToRect(inputImage.extent()), forKey: kCIInputImageKey)
whiteSpecksFilter.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputRVector")
whiteSpecksFilter.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputGVector")
whiteSpecksFilter.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputBVector")
whiteSpecksFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBiasVector")
// 3.把CISepiaTone滤镜和白斑图滤镜以源覆盖(source over)的方式先组合起来
let sourceOverCompositingFilter = CIFilter(name: "CISourceOverCompositing")
sourceOverCompositingFilter.setValue(whiteSpecksFilter.outputImage, forKey: kCIInputBackgroundImageKey)
sourceOverCompositingFilter.setValue(sepiaToneFilter.outputImage, forKey: kCIInputImageKey)
// ---------上面算是完成了一半
// 4.用CIAffineTransform滤镜先对随机噪点图进行处理
let affineTransformFilter = CIFilter(name: "CIAffineTransform")
affineTransformFilter.setValue(CIFilter(name: "CIRandomGenerator").outputImage.imageByCroppingToRect(inputImage.extent()), forKey: kCIInputImageKey
affineTransformFilter.setValue(NSValue(CGAffineTransform: CGAffineTransformMakeScale(1.5, 25)), forKey: kCIInputTransformKey)
// 5.建立蓝绿色磨砂图滤镜
let darkScratchesFilter = CIFilter(name: "CIColorMatrix")
darkScratchesFilter.setValue(affineTransformFilter.outputImage, forKey: kCIInputImageKey)
darkScratchesFilter.setValue(CIVector(x: 4, y: 0, z: 0, w: 0), forKey: "inputRVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputGVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputBVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 0, z: 0, w: 0), forKey: "inputAVector")
darkScratchesFilter.setValue(CIVector(x: 0, y: 1, z: 1, w: 1), forKey: "inputBiasVector")
// 6.用CIMinimumComponent滤镜把蓝绿色磨砂图滤镜处理成黑色磨砂图滤镜
let minimumComponentFilter = CIFilter(name: "CIMinimumComponent")
minimumComponentFilter.setValue(darkScratchesFilter.outputImage, forKey: kCIInputImageKey)
// ---------上面算是基本完成了
// 7.最终组合在一块儿
let multiplyCompositingFilter = CIFilter(name: "CIMultiplyCompositing")
multiplyCompositingFilter.setValue(minimumComponentFilter.outputImage, forKey: kCIInputBackgroundImageKey)
multiplyCompositingFilter.setValue(sourceOverCompositingFilter.outputImage, forKey: kCIInputImageKey)
// 8.最后输出
let outputImage = multiplyCompositingFilter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
以上就是一个老电影滤镜的“配方”了。
编译、运行,显示效果以下:
有时可能会对一些图片应用一样的滤镜,咱们可能会像上面那样把一连串的滤镜组合起来,以达到本身想要的效果,那么咱们就能够把这些操做封装到一个CIFilter的子类中,而后在多个地方反复使用,就像使用Core Image预置的滤镜那样。
CICategoryColorEffect类别中有个CIColorInvert滤 镜,这个滤镜提供反色功能,实现起来并不复杂,由于咱们并非作一个真正的自定义滤镜,而是在里面对Core Image已有滤镜的封装,咱们能够为子类定义一些输入参数,参照苹果对CIFilter子类的命名约定,输入参数必须用input做前缀,如 inputImage,而后再重写outputImage方法就好了。
如今咱们回到Xcode中,作如下几件事:
新建一个Cocoa Touch Class,类名就叫CIColorInvert,继承自CIFilter。
添加一个inputImage参数,类型天然是CIImage,由外界赋值。
重 写outputImage属性的getter。若是你以前写过Objective-C,应该对属性有这样一个印象:子类要重写父类的属性,只须要单独写个 getter或setter方法就好了,但在Swift里,不能经过这种方式重写属性,必须连getter、setter(若是父类的属性支持 setter的话)一块儿重写。在咱们的例子中outputImage在CIFilter中只是一个getter属性,
在outputImage里经过CIColorMatrix滤镜对图像的各向量进行调整。
CIColorInvert类实现:
class CIColorInvert: CIFilter {
var inputImage: CIImage!
override var outputImage: CIImage! {
get {
return CIFilter(name: "CIColorMatrix", withInputParameters: [
kCIInputImageKey : inputImage,
"inputRVector" : CIVector(x: -1, y: 0, z: 0),
"inputGVector" : CIVector(x: 0, y: -1, z: 0),
"inputBVector" : CIVector(x: 0, y: 0, z: -1),
"inputBiasVector" : CIVector(x: 1, y: 1, z: 1),
]).outputImage
}
}
}
而后在Storyboard的VC上增长一个按钮“反色”,链接到VC的colorInvert方法上,colorInvert方法实现以下:
@IBAction func colorInvert() {
let colorInvertFilter = CIColorInvert()
colorInvertFilter.inputImage = CIImage(image: imageView.image)
let outputImage = colorInvertFilter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
这 样一下,一个对Core Image预置滤镜的简单封装就完成了,每个滤镜的效果就像是一张配方,CIFilter就是装有配方的瓶子,因此子类化CIFilter并不算自定义 滤镜,可是从iOS 8开始,Core Image是支持真正的自定义滤镜的,自定义的滤镜被称之为内核(CIKernel),在WWDC视频里对其有50分钟的介绍:https://developer.apple.com/videos/wwdc/2014/#515。
运行后反色的效果,再次点击反色按钮后显示原图:
利用Core Image预置的滤镜能知足大部分使用场景,咱们作一个简单的替换背景的功能。
为了方便测试,加入两张新的图:
点击图片能够打开原图。
将两张图添加到当前工程中,而后把ViewController的属性originalImage改成返回左边的图:
......
lazy var originalImage: UIImage = {
return UIImage(named: "Image2")
}()
......
而后在Storyboard的VC上增长两个按钮:一个用于显示原图:
@IBAction func showOriginalImage() {
self.imageView.image = originalImage
}
另外一个按钮就叫“更换背景”,链接到VC的IBAction方法replaceBackground上。
咱们先看要作的事情:
消除深绿色
组合图片
就像Photoshop的魔法棒同样,Core Image也有相似的滤镜,可是没有那么简单粗暴,使用起来很麻烦。
在Core Image里,咱们为了消除某种颜色,须要使用CIColorCube滤镜,而CIColorCube滤镜须要一张cube映射表,这张表其实就是张颜色表(3D颜色查找表),把你想消除的颜色的alpha值设置为0,其余的颜色不变,Core Image将会把图像数据上的颜色映射为表中的颜色,以此来达到消除某种颜色的目的。
CIColorCube的这张表默认不会对inputImage做任何处理,但在咱们这里要将全部的深绿色干掉,因此须要本身来创建这张表。
咱们要消除的“深绿色”并不仅是视觉上的一种颜色,而是颜色的范围,最直接的方法是将RGBA转成HSV(Hue,Saturation,Value), 在HSV的格式下,颜色是围绕圆柱体中轴的角度来表现的,在这种表现方法下,你能把颜色的范围想象成连在一块儿的扇形,而后直接把该块区域干掉(alpha 设为0),这就表示咱们实际上须要指定颜色区域的范围------围绕圆柱体中轴线的最小角度以及最大角度,此范围内的颜色alpha设为0。最 后,Cube Map表中的数据必须乘以alpha,因此建立Cube Map的最后一步是把RGB值乘以你刚刚计算出来的alpha值:若是是想要消除的颜色,乘出来就是0,反之则不变。这是一张表明颜色值区域的HSV(Hue值)图:
能够看到若是是纯绿色,其取值是120度,蓝色是240度,咱们这种状况取值大概在60到90左右(偏绿一点),在这个网站上能够看到更详细的RGB颜色对应的HSV值。
那么接下来咱们就准备建立Cube Map表,建立Cube Map表的方法在苹果官方示例中能够找到,是C语言实现的,为了方便起见,咱们就直接建立一个C文件来包含这些代码。
在工程里选择新建一个.c文件,我取名为CubeMap.c,在建立这个.c文件的时候,不出意外的话Xcode会问你是否须要建立一个桥接头文件(xxxx.Bridging-Header.H),选择是,Xcode会建立该文件,并自动把其路径放到编译选项的Objective-C Bridging Header中。若是你要本身添加这个文件,而且须要手动修改Objective-C Bridging Header的编译选项,能够看这里。
.c文件搞完之后,即把苹果官方示例中的代码(如下代码)添加进去:
struct CubeMap {
int length;
float dimension;
float *data;
};
struct CubeMap createCubeMap(float minHueAngle, float maxHueAngle) {
const unsigned int size = 64;
struct CubeMap map;
map.length = size * size * size * sizeof (float) * 4;
map.dimension = size;
float *cubeData = (float *)malloc (map.length);
float rgb[3], hsv[3], *c = cubeData;
for (int z = 0; z < size; z++){
rgb[2] = ((double)z)/(size-1); // Blue value
for (int y = 0; y < size; y++){
rgb[1] = ((double)y)/(size-1); // Green value
for (int x = 0; x < size; x ++){
rgb[0] = ((double)x)/(size-1); // Red value
rgbToHSV(rgb,hsv);
// Use the hue value to determine which to make transparent
// The minimum and maximum hue angle depends on
// the color you want to remove
float alpha = (hsv[0] > minHueAngle && hsv[0] < maxHueAngle) ? 0.0f: 1.0f;
// Calculate premultiplied alpha values for the cube
c[0] = rgb[0] * alpha;
c[1] = rgb[1] * alpha;
c[2] = rgb[2] * alpha;
c[3] = alpha;
c += 4; // advance our pointer into memory for the next color value
}
}
}
map.data = cubeData;
return map;
}
我将这个方法稍微改造了一下,选回一个结构体,由于外面要用到length和dimension。苹果没有提供rgbToHSV方法的实现,能够用我找到的这个:
void rgbToHSV(float *rgb, float *hsv) {
float min, max, delta;
float r = rgb[0], g = rgb[1], b = rgb[2];
float *h = hsv, *s = hsv + 1, *v = hsv + 2;
min = fmin(fmin(r, g), b );
max = fmax(fmax(r, g), b );
*v = max;
delta = max - min;
if( max != 0 )
*s = delta / max;
else {
*s = 0;
*h = -1;
return;
}
if( r == max )
*h = ( g - b ) / delta;
else if( g == max )
*h = 2 + ( b - r ) / delta;
else
*h = 4 + ( r - g ) / delta;
*h *= 60;
if( *h < 0 )
*h += 360;
}
我在.c文件中导入的库:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
对了,若是那个桥接文件里没有导入这个.c文件的话是不行的,Swift的类会找不到这里面的方法。
// ComplexFilters-Bridging-Header.h
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "CubeMap.c"
VC中的replaceBackground方法只须要作三件事:
建立Cube Map表
建立CIColorCube滤镜并使用Cube Map
用CISourceOverCompositing滤镜将处理过的人物图像和未处理过的背景图粘合起来
方法实现以下:
@IBAction func replaceBackground() {
let cubeMap = createCubeMap(60,90)
let data = NSData(bytesNoCopy: cubeMap.data, length: Int(cubeMap.length), freeWhenDone: true)
let colorCubeFilter = CIFilter(name: "CIColorCube")
colorCubeFilter.setValue(cubeMap.dimension, forKey: "inputCubeDimension")
colorCubeFilter.setValue(data, forKey: "inputCubeData")
colorCubeFilter.setValue(CIImage(image: imageView.image), forKey: kCIInputImageKey)
var outputImage = colorCubeFilter.outputImage
let sourceOverCompositingFilter = CIFilter(name: "CISourceOverCompositing")
sourceOverCompositingFilter.setValue(outputImage, forKey: kCIInputImageKey)
sourceOverCompositingFilter.setValue(CIImage(image: UIImage(named: "background")), forKey: kCIInputBackgroundImageKey)
outputImage = sourceOverCompositingFilter.outputImage
let cgImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
imageView.image = UIImage(CGImage: cgImage)
}
参数设置都还比较简单,CISourceOverCompositing滤镜目前已经使用过屡次了,并无什么复杂的。
编译、运行,能够分两次执行,先看消除深绿色的效果,再看最后使用CISourceOverCompositing滤镜组合图片以后的效果:
我在GitHub上会保持更新。
我在更换背景的右侧,新加入了一个显示图2的button,已在GitHub上更新。
参考资料: