Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

Ncnn使用详解(2)——Android端 - DmrfCoder的博客 - CSDN博客

$
0
0

摘要

本片文章基于你已经完成了 这篇文章的学习,主要介绍如何将写好的c代码应用到Android项目中。

环境说明

系统:Ubuntu16.04 
软件:Android Studio

前期准备之 ndk安装

在正式开始前我们需要先下载安装ndk,这里介绍一种简单高效的方式,打开Android Studio,然后依次点击File->Settings->Appearance&Behavior->System Settings->Android SDK,然后在SDK Tools下找到ndk,然后选中,点击apply就可以自动下载安装了,如图: 
这里写图片描述
完成之后在你的sdk目录下会多出一个ndk-bundle的包,这就是你ndk的路径,类似下图: 
这里写图片描述
至此,ndk已经安装完毕了,下一步是配置ndk的环境变量: 
首先打开profile:

sudo vim /etc/profile
  • 1

打开后在profile文件的末尾加上:

export NDK_HOME=sdkroot/ndk-bundle
PATH=$NDK_HOME:$PATH
  • 1
  • 2

sdkroot是你的sdk目录,每个人的不一样,视情况而定,下面是我的配置 截图: 
这里写图片描述
添加完成后保存退出,使用以下命令使配置的环境变量生效:

source /etc/profile
  • 1

验证ndk是否配置成功:

ndk-build -v
  • 1

出现类似以下输出即说明ndk配置成功: 
这里写图片描述

编译ncnn sdk

我们需要将ncnn打包,这样我们才能在android ndk的代码中使用include

mkdir build-android
cd build-android
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI="armeabi-v7a" -DANDROID_ARM_NEON=ON \
    -DANDROID_PLATFORM=android-14 ..
make
make install
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

参数说明:

ANDROID_ABI 是架构名字,"armeabi-v7a" 支持绝大部分手机硬件

ANDROID_ARM_NEON 是否使用 NEON 指令集,设为 ON 支持绝大部分手机硬件

ANDROID_PLATFORM 指定最低系统版本,"android-14" 就是 android-4.0
  • 1
  • 2
  • 3
  • 4
  • 5

你可以根据自己的需要设置自己的参数,详见 这里 
完成后你就可以在ncnn/build-android下找到install了,大概如图: 
这里写图片描述
install下有include和lib两个文件夹,这两个文件夹下的东西后面会用到。

进行ndk开发

Android可以通过ndk-build和cmake两种方式来编译c,而且官方比较推荐的是cmake的方式,但是我用cmake试了好长时间一直报各种诡异的错误,应该是我还没有学到ncnn in ndk with cmake的正确打开方式,所以这里介绍一下使用ndk-build这种方式编译c,步骤如下: 
我这里新建了一个android 的demo项目,项目结构如下: 
这里写图片描述 
主要是assets文件夹下放置你的bin和param文件,jni文件夹下放置你的cpp和两个mk文件,具体内容下面会介绍(可以直接在对应位置新建这两个文件夹),然后要修改你的app gradle文件: 
这里写图片描述
对应的内容你可以根据自己的情况修改,然后配置两个mk文件: 
* Android.mk

LOCAL_PATH := $(call my-dir)

#把这个路径改成你自己刚才编译的install路径,用全路径!
NCNN_INSTALL_PATH := ncnn-master/build-android/install

include $(CLEAR_VARS)
LOCAL_MODULE := ncnn
LOCAL_SRC_FILES := $(NCNN_INSTALL_PATH)/lib/libncnn.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE := demo
#这个是你的cpp文件
LOCAL_SRC_FILES := demo.cpp

LOCAL_C_INCLUDES := $(NCNN_INSTALL_PATH)/include

LOCAL_STATIC_LIBRARIES := ncnn

LOCAL_CFLAGS := -O2 -fvisibility=hidden -fomit-frame-pointer -fstrict-aliasing -ffunction-sections -fdata-sections -ffast-math
LOCAL_CPPFLAGS := -O2 -fvisibility=hidden -fvisibility-inlines-hidden -fomit-frame-pointer -fstrict-aliasing -ffunction-sections -fdata-sections -ffast-math
LOCAL_LDFLAGS += -Wl,--gc-sections

LOCAL_CFLAGS += -fopenmp
LOCAL_CPPFLAGS += -fopenmp
LOCAL_LDFLAGS += -fopenmp

LOCAL_LDLIBS := -lz -llog -ljnigraphics

include $(BUILD_SHARED_LIBRARY)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • Application.mk

# APP_STL := stlport_static
APP_STL := gnustl_static
# APP_ABI := armeabi armeabi-v7a
APP_ABI := armeabi-v7a
APP_PLATFORM := android-9
#NDK_TOOLCHAIN_VERSION := 4.9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这两个.mk文件我的建议是复制粘贴到你的项目里,只改动必要的文件路径,其余的参数别动,除非你直到你改的意思是什么~ 
因为ndk的原理是使用 java接口调用c代码,所以我们需要进行java接口的编写,给出一个示例代码:

public class Ncnn
{
    public native boolean InitNcnn(String gestureDetectionModelPath);


    public native  void Detect(float[] i,float[] q,float []scores,int[] a);


    static {
        System.loadLibrary("demo");
    }



}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

一共两个方法,一个是初始化,一个是执行预测,需要注意的是初始化方法调用的时候需要传入一个二进制文件路径的参数,大概思路是把bin和param文件拷贝到手机上然后让c代码读取,这里给出模板代码:

 private void IniteNcnn() throws IOException {


        ncnn = new Ncnn();//实例化上面的java接口类


        try {
            copyBigDataToSD("demo.bin");

            copyBigDataToSD("demo.param");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //模型初始化
        File sdDir = Environment.getExternalStorageDirectory();//获取跟目录
        String sdPath = sdDir.toString() + "/gesturencnn/";
        boolean b = ncnn.InitNcnn(sdPath);


    }


    private void copyBigDataToSD(String strOutFileName) throws IOException {

        File sdDir = Environment.getExternalStorageDirectory();
        File file = new File(sdDir.toString() + "/demo/");
        if (!file.exists()) {
            file.mkdir();
        }

        String tmpFile = sdDir.toString() + "/demo/" + strOutFileName;
        File f = new File(tmpFile);
        if (f.exists()) {
            return;
        }
        InputStream myInput;
        java.io.OutputStream myOutput = new FileOutputStream(sdDir.toString() + "/demo/" + strOutFileName);
        myInput = context.getAssets().open(strOutFileName);
        byte[] buffer = new byte[1024];
        int length = myInput.read(buffer);
        while (length > 0) {
            myOutput.write(buffer, 0, length);
            length = myInput.read(buffer);
        }
        myOutput.flush();
        myInput.close();
        myOutput.close();

    }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

直接在你需要的地方调用IniteNcnn()就可以了,需要注意的是要 进行内存卡读取权限的申请

然后将你上一篇教程写的demo.cpp放在jni目录下,对里面的代码进行必要的修改,主要需要实现模型的初始化和执行预测两个函数,初始化这里给出一个模板,至于执行预测的函数直接写一个对应函数然后调用你之前写好的c代码就可以了:

  • 初始化 
    这个函数是通用的,建议全部复制粘贴,具体对应的java接口代码后面会介绍

extern "C"{

#注意把这里的函数名改成你自己对应的,一定不能错!
JNIEXPORT jboolean JNICALL
Java_com_example_dmrf_JniClass_Ncnn_InitNcnn(JNIEnv *env, jobject instance,
                                                                    jstring DetectionModelPath_) {


    const char *DetectionModelPath = env->GetStringUTFChars(DetectionModelPath_, 0);
    if (NULL == DetectionModelPath) {
        return false;
    }

    string tModelDir = DetectionModelPath;
    string tLastChar = tModelDir.substr(tModelDir.length() - 1, 1);


    if ("\\" == tLastChar) {
        tModelDir = tModelDir.substr(0, tModelDir.length() - 1) + "/";
    } else if (tLastChar != "/") {
        tModelDir += "/";
    }


    std::vector<std::string> param_files;
    param_files.resize(1);
    param_files[0] = tModelDir + "/demo.param";

    std::vector<std::string> bin_files;
    bin_files.resize(1);
    bin_files[0] = tModelDir + "/demo.bin";


    squeezenet.load_param(param_files[0].data());
    squeezenet.load_model(bin_files[0].data());

    env->ReleaseStringUTFChars(DetectionModelPath_, DetectionModelPath);

    return true;
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

至此所有代码已经编写完毕,然后就可以build了,步骤如下: 
cd到src/main/jni目录下,执行ndk-build,然后就会生成.so文件,然后你就可以干你的后序工作了。


.Net Core in Docker - 在容器内编译发布并运行 - Agile.Zhou - 博客园

$
0
0

Docker可以说是现在微服务,DevOps的基础,咱们.Net Core自然也得上Docker。.Net Core发布到Docker容器的教程网上也有不少,但是今天还是想来写一写。
你搜.Net core程序发布到Docker网上一般常见的有两种方案:

  • 1、在本地编译成Dll文件后通过SCP命令或者WinSCP等工具上传到服务器上,然后构建Docker镜像再运行容器。该方案跟传统的发布很像,麻烦的地方是每次都要打开相关工具往服务器上复制文件。
  • 2、在服务端直接通过Git获取最新源代码后编译成Dll然后构建Docker镜像再运行容器。该方案免去了往服务器复制文件这步操作,但是服务器环境需要安装.Net Core SDK 来编译源代码。
    自从用了Docker简直懒的不能自理,我既不想手工复制文件到服务器,也不想在服务器装.Net Core环境。显然只要Docker镜像包含.Net Core SDK环境就可以在Docker内帮我们编译代码然后运行,这样连我们的服务器都不用装啥.Net Core的环境拉。

    在Docker内编译发布.Net Core程序并运行

    新建一个Asp.net Core MVC项目

    我们使用一个Asp.net Core MVC程序来演示如何发布到Docker并运行。
    新建项目
    使用vs新建一个Asp.net core mvc项目
public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return Content($"Core for docker , {DateTime.Now} , verson 2");
        }
    }

修改HomeController下的index Action,直接输出一段文字

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .UseKestrel(op =>
            {
                op.ListenAnyIP(5000);
            })
            .UseStartup<Startup>();

修改Program下的CreateWebHostBuilder方法,让Kestrel监听5000端口


本地运行一下试试

推送源码到代码仓库

把我们的代码推送到对应的Git仓库,方便我们从部署服务器上直接拉取最新的代码。

X:\workspace\CoreForDocker>git remote add origin https://gitee.com/kklldog/CoreForDocker.git

X:\workspace\CoreForDocker>git push -u origin master
Username for 'https://gitee.com': xxx@gmail.com
Password for 'https://xxx@gmail.com@gitee.com':
Counting objects: 88, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (83/83), done.
Writing objects: 100% (88/88), 527.07 KiB | 2.43 MiB/s, done.
Total 88 (delta 7), reused 0 (delta 0)
remote: Powered By Gitee.com
To https://gitee.com/kklldog/CoreForDocker.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

添加Dockerfile文件

在CoreForDocker下新增一个Dockerfile文件,注意没有任何扩展名。我们需要基于microsoft/dotnet:latest这个镜像构建一个新的镜像。并且在构建的过程中直接对源码进行编译并发布。

FROM microsoft/dotnet:latest
WORKDIR /app
COPY /. /app
RUN dotnet restore
RUN dotnet publish -o /out -c Release
EXPOSE 5000
ENTRYPOINT ["dotnet", "/out/CoreForDocker.dll"]

大概解释下Dockerfile的意思:
FROM microsoft/dotnet:latest: 使用dotnet的最新镜像,这个镜像其实对应的应该就是2.2-sdk这个镜像,里面包含了dotnet-core 2.2 sdk
WORKDIR /app: 指定工作目录为app
COPY /. /app复制宿主机当前目录的内容到容器的app文件夹
RUN dotnet restore: 还原nuget包
RUN dotnet publish -o /out -c Release编译并发布程序集到容器的out目录
EXPOSE 5000: 暴露5000端口
ENTRYPOINT ["dotnet", "/out/CoreForDocker.dll"]: 容器启动的时候执行dotnet命令,参数为/out/CoreForDocker.dll


Dockerfile的文件属性设置为始终复制
新建好Dockerfile后git push到代码仓库。

在服务器上构建Docker镜像

这里以Ubuntu为例,ssh登录到服务器后使用git clone命令拉取源代码。

git clone https://gitee.com/kklldog/CoreForDocker.git

进入源码目录

cd CodeForDocker\CodeForDocker

使用docker build命令构建新的镜像,注意不要忘记最后一个'.'

docker build -t image_code4docker .

运行容器

如果以上步骤都没有报错,那么恭喜你镜像已经构建成功了,我们可以使用此镜像运行Docker容器了。

docker run -d --name code4docker -p 5000:5000 -v /ect/localtime:/ect/localtime image_core4docker

使用image_core4docker镜像运行一个名为core4docker的容器,绑定宿主机的5000到容器的5000口。其中需要注意的是-v参数映射宿主机的/ect/localtime文件夹到容器的/ect/localtime文件夹,因为经过实践发现容器中的时区有可能跟宿主机不一致,需要映射宿主机的/ect/localtime让容器的时区跟宿主机保持一致。


访问一下服务器的5000端口,发现能够正确返回数据表示我们的Asp.net Core程序在容器中运行成功了

以后当我们对源码进行修改,并提交后,我们只需在服务器上拉取最新的代码然后使用docker build,docker run命令来再次生成镜像并运行容器。但是手工输入docker build,docker run的命令好像也很麻烦,参数又那么多,太烦了。

使用shell脚本简化操作

为了偷懒不想敲那么长的命令,我们可以构建一个脚本,把命令一次性写好,以后只要运行一次脚本就可以了。
使用vim新建一个publish.sh的文件

vim publish.sh

键盘上按i进入编辑模式,输入以下内容

cd CoreForDocker/CoreForDocker
git pull
docker stop core4docker
docker rm core4docker
docker rmi image_core4docker
docker build -t image_core4docker .
docker run --name core4docker -d -p 5000:5000 -v /etc/localtime:/etc/localtime image_core4docker

以上命令,不光有新建镜像跟运行容器的命令,还有移除原来的容器跟镜像的命令
按ecs进入命令模式,退出保存

:wq

让我们模拟修改一下源代码,并提交到代码仓库

public IActionResult Index()
    {
        return Content($"Core for docker , {DateTime.Now} , version 2");
    }

再次修改homecontroller的index action,输出内容上新增一个version
ssh登录到服务器,运行publish.sh文件

/bin/bash publish.sh


跑完之后我们再次访问下服务器的5000口,数据返回正确,表示服务器上跑的已经是最新的程序了

总结

通过以上演示我们基本了解如何通过git跟docker配合在Ubuntu服务器上不安装.Net Core SDK来发布.Net Core 程序到容器中运行,并且通过shell脚本的方式再次简化发布。但是尽管这样每次发布都需要ssh到服务器上然后运行脚本,特别是开发环境可能经常需要发布,还是觉得麻烦。有没有什么办法让我们push代码后服务器自动就开始部署最新的代码的到容器中运行了呢?
后面我会介绍下如何通过jenkins跟webhook来做CICD。

我从来不理解 JavaScript 闭包,直到有人这样向我解释它... - Java架构—月亮 - 博客园

$
0
0

正如标题所述,JavaScript 闭包对我来说一直有点神秘,看过很多闭包的文章,在工作使用过闭包,有时甚至在项目中使用闭包,但我确实是这是在使用闭包的知识。

最近看国外的一些文章,终于,有人用于一种让我明白方式对闭包进行了解释,我将在本文中尝试使用这种方法来解释闭包。

准备

在理解闭包之前,有个重要的概念需要先了解一下,就是 js 执行上下文。

这篇 文章是执行上下文 很不错的入门教程,文章中提到:

当代码在 JavaScript 中运行时,执行代码的环境非常重要,并将概括为以下几点:

全局代码——第一次执行代码的默认环境。

函数代码——当执行流进入函数体时。

(…) —— 我们当作 执行上下文 是当前代码执行的一个环境与范围。

换句话说,当我们启动程序时,我们从全局执行上下文中开始。一些变量是在全局执行上下文中声明的。我们称之为全局变量。当程序调用一个函数时,会发生什么?

以下几个步骤:

  • JavaScript 创建一个新的执行上下文,我们叫作本地执行上下文。
  • 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
  • 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。

函数什么时候结束?当它遇到一个 return 语句或一个结束括号}。

当一个函数结束时,会发生以下情况:

  • 这个本地执行上下文从执行堆栈中弹出。
  • 函数将返回值返回调用上下文。调用上下文是调用这个本地的执行上下文,它可以是全局执行上下文,也可以是另外一个本地的执行上下文。这取决于调用执行上下文来处理此时的返回值,返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有 return 语句,则返回 undefined。
  • 这个本地执行上下文被销毁,销毁是很重要,这个本地执行上下文中声明的所有变量都将被删除,不在有变量,这个就是为什么 称为本地执行上下文中自有的变量。

基础的例子

在讨论闭包之前,让我们看一下下面的代码:

1:let a =3            
2:functionaddTwo(x) {
3:let ret = x +2
4:return ret
5: }
6:let b = addTwo(a)
7:console.log(b)

为了理解 JavaScript 引擎是如何工作的,让我们详细分析一下:

  • 在第 1 行,我们在全局执行上下文中声明了一个新变量 a,并将赋值为 3。
  • 接下来就变得棘手了,第 2 行到第 5 行实际上是在一起的。这里发生了什么? 我们在全局执行上下文中声明了一个名为 addTwo的新变量,我们给它分配了什么?一个函数定义。两个括号{}之间的任何内容都被分配给 addTwo,函数内部的代码没有被求值,没有被执行,只是存储在一个变量中以备将来使用。
  • 现在我们在第 6 行。它看起来很简单,但是这里有很多东西需要拆开分析。首先,我们在全局执行上下文中声明一个新变量,并将其标记为 b,变量一经声明,其值即为 undefined。
  • 接下来,仍然在第 6 行,我们看到一个赋值操作符。我们准备给变量 b赋一个新值,接下来我们看到一个函数被调用。当您看到一个变量后面跟着一个圆括号(…)时,这就是调用函数的信号,接着,每个函数都返回一些东西(值、对象或 undefined),无论从函数返回什么,都将赋值给变量 b
  • 但是首先我们需要调用标记为 addTwo的函数。JavaScript 将在其全局执行上下文内存中查找名为 addTwo的变量。噢,它找到了一个,它是在步骤 2(或第 2 - 5 行)中定义的。变量 add2包含一个函数定义。注意,变量 a作为参数传递给函数。JavaScript 在全局执行上下文内存中搜索变量 a,找到它,发现它的值是 3,并将数字 3 作为参数传递给函数,准备好执行函数。
  • 现在执行上下文将切换,创建了一个新的本地执行上下文,我们将其命名为“addTwo 执行上下文”,执行上下文被推送到调用堆栈上。在 addTwo 执行上下文中,我们要做的第一件事是什么?
  • 你可能会说,“在 addTwo 执行上下文中声明了一个新的变量 ret”,这是不对的。正确的答案是,我们需要先看函数的参数。在 addTwo 执行上下文中声明一个新的变量 x`,因为值 3 是作为参数传递的,所以变量 x 被赋值为 3。
  • 下一步是:在 addTwo 执行上下文中声明一个新的变量 ret。它的值被设置为 undefined(第三行)。
  • 仍然是第 3 行,需要执行一个相加操作。首先我们需要 x的值,JavaScript 会寻找一个变量 x,它会首先在 addTwo执行上下文中寻找,找到了一个值为 3。第二个操作数是数字 2。两个相加结果为 5 就被分配给变量 ret
  • 第 4 行,我们返回变量 ret的内容,在 addTwo 执行上下文中查找,找到值为 5,返回,函数结束。
  • 第 4 - 5 行,函数结束。addTwo 执行上下文被销毁,变量 xret被消去了,它们已经不存在了。addTwo 执行上下文从调用堆栈中弹出,返回值返回给调用上下文,在这种情况下,调用上下文是全局执行上下文,因为函数 addTwo 是从全局执行上下文调用的。
  • 现在我们继续第 4 步的内容,返回值 5 被分配给变量 b,程序仍然在第 6 行。
  • 在第 7 行, b的值 5 被打印到控制台了。

对于一个非常简单的程序,这是一个非常冗长的解释,我们甚至还没有涉及闭包。但肯定会涉及的,不过首先我们得绕一两个弯。

词法作用域(Lexical scope)

我们需要理解词法作用域的一些知识。请看下面的例子:

1:let val1 =2            
2:functionmultiplyThis(n) {
3:let ret = n * val1
4:return ret
5: }
6:let multiplied = multiplyThis(6)
7:console.log('example of scope:', multiplied)

这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript 的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)。

下面列出向个步骤来解释一下(如果你已经熟悉了,请跳过):

  • 在全局执行上下文中声明一个新的变量 val1,并将其赋值为 2。
  • 行 2 - 5,声明一个新的变量  multiplyThis,并给它分配一个函数定义。
  • 第六行,声明一个在全局执行上下文  multiplied 新变量。
  • 从全局执行上下文内存中查找变量 multiplyThis,并将其作为函数执行,传递数字 6 作为参数。
  • 新函数调用(创建新执行上下文),创建一个新的  multiplyThis 函数执行上下文。
  • 在  multiplyThis 执行上下文中,声明一个变量 n 并将其赋值为 6
  • 第 3 行。在 multiplyThis执行上下文中,声明一个变量 ret
  • 继续第 3 行。对两个操作数 n 和 val1 进行乘法运算.在 multiplyThis执行上下文中查找变量  n。我们在步骤 6 中声明了它,它的内容是数字 6。在 multiplyThis执行上下文中查找变量 val1multiplyThis执行上下文没有一个标记为 val1 的变量。我们向调用上下文查找,调用上下文是全局执行上下文,在全局执行上下文中寻找  val1。哦,是的、在那儿,它在步骤 1 中定义,数值是 2。
  • 继续第 3 行。将两个操作数相乘并将其赋值给 ret变量,6 * 2 = 12,ret 现在值为 12。
  • 返回 ret变量,销毁 multiplyThis执行上下文及其变量  ret 和  n 。变量  val1 没有被销毁,因为它是全局执行上下文的一部分。
  • 回到第 6 行。在调用上下文中,数字 12 赋值给  multiplied 的变量。
  • 最后在第 7 行,我们在控制台中打印  multiplied 变量的值

在这个例子中,我们需要记住一个函数可以访问在它的调用上下文中定义的变量,这个就是词法作用域(Lexical scope)。

返回函数的函数

在第一个例子中,函数 addTwo返回一个数字。请记住,函数可以返回任何东西。让我们看一个返回函数的函数示例,因为这对于理解闭包非常重要。看粟子:

1:let val =7            
2:functioncreateAdder() {
3:functionaddNumbers(a, b) {
4:let ret = a + b
5:return ret
6: }
7:return addNumbers
8: }
9:let adder = createAdder()
10:let sum = adder(val,8)
11:console.log('example of function returning a function: ', sum)

让我们回到分步分解:

  • 第一行。我们在全局执行上下文中声明一个变量 val并赋值为 7。
  • 行 2 - 8。我们在全局执行上下文中声明了一个名为  createAdder 的变量,并为其分配了一个函数定义。第 3 至 7 行描述了上述函数定义,和以前一样,在这一点上,我们没有直接讨论这个函数。我们只是将函数定义存储到那个变量( createAdder)中。
  • 第 9 行。我们在全局执行上下文中声明了一个名为  adder 的新变量,暂时,值为 undefined。
  • 第 9 行。我们看到括号(),我们需要执行或调用一个函数,查找全局执行上下文的内存并查找名为 createAdder 的变量,它是在步骤 2 中创建的。好吧,我们调用它。
  • 调用函数时,执行到第 2 行。创建一个新的 createAdder执行上下文。我们可以在 createAdder的执行上下文中创建自有变量。js 引擎将 createAdder的上下文添加到调用堆栈。这个函数没有参数,让我们直接跳到它的主体部分.
  • 第 3 - 6 行。我们有一个新的函数声明,我们在 createAdder执行上下文中创建一个变量 addNumbers。这很重要, addnumber只存在于 createAdder执行上下文中。我们将函数定义存储在名为  addNumbers` 的自有变量中。
  • 在第 7 行,我们返回变量 addNumbers的内容。js 引擎查找一个名为 addNumbers的变量并找到它,这是一个函数定义。好的,函数可以返回任何东西,包括函数定义。我们返 addNumbers的定义。第 4 行和第 5 行括号之间的内容构成该函数定义。
  • 返回时, createAdder执行上下文将被销毁。 addNumbers 变量不再存在。但 addNumbers函数定义仍然存在,因为它返回并赋值给了 adder 变量。
  • 第 10 行。我们在全局执行上下文中定义了一个新的变量  sum,先负值为 undefined;
  • 接下来我们需要执行一个函数。哪个函数?是名为 adder变量中定义的函数。我们在全局执行上下文中查找它,果然找到了它,这个函数有两个参数。
  • 让我们查找这两个参数,第一个是我们在步骤 1 中定义的变量 val,它表示数字 7,第二个是数字 8。
  • 现在我们要执行这个函数,函数定义概述在第 3-5 行,因为这个函数是匿名,为了方便理解,我们暂且叫它 adder吧。这时创建一个 adder函数执行上下文,在 adder执行上下文中创建了两个新变量  a 和  b。它们分别被赋值为 7 和 8,因为这些是我们在上一步传递给函数的参数。
  • 第 4 行。在 adder执行上下文中声明了一个名为 ret的新变量,
  • 第 4 行。将变量 a的内容和变量 b的内容相加得 15 并赋给 ret 变量。
  • ret变量从该函数返回。这个匿名函数执行上下文被销毁,从调用堆栈中删除,变量 abret不再存在。
  • 返回值被分配给我们在步骤 9 中定义的 sum变量。
  • 我们将 sum的值打印到控制台。
  • 如预期,控制台将打印 15。我们在这里确实经历了很多困难,我想在这里说明几点。首先,函数定义可以存储在变量中,函数定义在程序调用之前是不可见的。其次,每次调用函数时,都会(临时)创建一个本地执行上下文。当函数完成时,执行上下文将消失。函数在遇到 return 或右括号}时执行完成。

码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具  Fundebug

最后,一个闭包

看看下面的代码,并试着弄清楚会发生什么。

1:functioncreateCounter() {            
2:let counter =0
3:const myFunction =function() {
4: counter = counter +1
5:return counter
6: }
7:return myFunction
8: }
9:const increment = createCounter()
10:const c1 = increment()
11:const c2 = increment()
12:const c3 = increment()
13:console.log('example increment', c1, c2, c3)

现在,我们已经从前两个示例中掌握了它的诀窍,让我们按照预期的方式快速执行它:

  • 行 1 - 8。我们在全局执行上下文中创建了一个新的变量 createCounter,并赋值了一个的函数定义。
  • 第 9 行。我们在全局执行上下文中声明了一个名为 increment的新变量。
  • 第 9 行。我们需要调用 createCounter函数并将其返回值赋给 increment变量。
  • 行 1 - 8。调用函数,创建新的本地执行上下文。
  • 第 2 行。在本地执行上下文中,声明一个名为 counter的新变量并赋值为 0;
  • 行 3 - 6。声明一个名为 myFunction的新变量,变量在本地执行上下文中声明,变量的内容是为第 4 行和第 5 行所定义。
  • 第 7 行。返回 myFunction变量的内容,删除本地执行上下文。变量 myFunctioncounter不再存在。此时控制权回到了调用上下文。
  • 第 9 行。在调用上下文(全局执行上下文)中, createCounter返回的值赋给了 increment,变量 increment现在包含一个函数定义内容为 createCounter返回的函数。它不再标记为 myFunction`,但它的定义是相同的。在全局上下文中,它是的标记为  labeledincrement
  • 第 10 行。声明一个新变量(c1)。
  • 继续第 10 行。查找 increment变量,它是一个函数并调用它。它包含前面返回的函数定义,如第 4-5 行所定义的。
  • 创建一个新的执行上下文。没有参数。开始执行函数。
  • 第 4 行。counter=counter + 1。在本地执行上下文中查找 counter变量。我们只是创建了那个上下文,从来没有声明任何局部变量。让我们看看全局执行上下文。这里也没有 counter变量。Javascript 会将其计算为 counter = undefined + 1,声明一个标记为 counter的新局部变量,并将其赋值为 number 1,因为 undefined 被当作值为 0。
  • 第 5 行。我们变量 counter的值(1),我们销毁本地执行上下文和 counter变量。
  • 回到第 10 行。返回值(1)被赋给 c1。
  • 第 11 行。重复步骤 10-14,c2 也被赋值为 1。
  • 第 12 行。重复步骤 10-14,c3 也被赋值为 1。
  • 第 13 行。我们打印变量 c1 c2 和 c3 的内容。

你自己试试,看看会发生什么。你会将注意到,它并不像从我上面的解释中所期望的那样记录 1,1,1。而是记录 1,2,3。这个是为什么?

不知怎么滴, increment函数记住了那个 cunter的值。这是怎么回事?

counter是全局执行上下文的一部分吗?尝试 console.log(counter),得到 undefined 的结果,显然不是这样的。

也许,当你调用 increment时,它会以某种方式返回它创建的函数(createCounter)?这怎么可能呢?变量 increment包含函数定义,而不是函数的来源,显然也不是这样的。

所以一定有另一种机制。闭包,我们终于找到了,丢失的那块。

它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变量。

所以我们上面的解释都是错的,让我们再试一次,但是这次是正确的。

1:functioncreateCounter() {            
2:let counter =0
3:const myFunction =function() {
4: counter = counter +1
5:return counter
6: }
7:return myFunction
8: }
9:const increment = createCounter()
10:const c1 = increment()
11:const c2 = increment()
12:const c3 = increment()
13:console.log('example increment', c1, c2, c3)
  • 同上, 行 1 - 8。我们在全局执行上下文中创建了一个新的变量 createCounter,它得到了指定的函数定义。
  • 同上,第 9 行。我们在全局执行上下文中声明了一个名为 increment的新变量。
  • 同上,第 9 行。我们需要调用 createCounter函数并将其返回值赋给 increment变量。
  • 同上,行 1 - 8。调用函数,创建新的本地执行上下文。
  • 同上,第 2 行。在本地执行上下文中,声明一个名为 counter的新变量并赋值为 0 。
  • 行 3 - 6。声明一个名为 myFunction的新变量,变量在本地执行上下文中声明,变量的内容是另一个函数定义。如第 4 行和第 5 行所定义,现在我们还创建了一个闭包,并将其作为函数定义的一部分。闭包包含作用域中的变量,在本例中是变量 counter(值为 0)。
  • 第 7 行。返回 myFunction变量的内容,删除本地执行上下文。 myFunctioncounter不再存在。控制权交给了调用上下文,我们返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量。
  • 第 9 行。在调用上下文(全局执行上下文)中, createCounter返回的值被指定为 increment,变量 increment现在包含一个函数定义(和闭包),由 createCounter 返回的函数定义,它不再标记为 myFunction,但它的定义是相同的,在全局上下文中,称为 increment
  • 第 10 行。声明一个新变量(c1)。
  • 继续第 10 行。查找变量 increment,它是一个函数,调用它。它包含前面返回的函数定义,如第 4-5 行所定义的。(它还有一个带有变量的闭包)。
  • 创建一个新的执行上下文,没有参数,开始执行函数。
  • 第 4 行。counter = counter + 1,寻找变量  counter,在查找本地或全局执行上下文之前,让我们检查一下闭包,瞧,闭包包含一个名为 counter的变量,其值为 0。在第 4 行表达式之后,它的值被设置为 1。它再次被储存在闭包里,闭包现在包含值为 1 的变量  counter
  • 第 5 行。我们返回 counter的值,销毁本地执行上下文。
  • 回到第 10 行。返回值(1)被赋给变量 c1
  • 第 11 行。我们重复步骤 10-14。这一次,在闭包中此时变量 counter的值是 1。它在第 12 步设置的,它的值被递增并以 2 的形式存储在递增函数的闭包中,c2 被赋值为 2。
  • 第 12 行。重复步骤 10-14, c3被赋值为 3。
  • 第 13 行。我们打印变量 c1 c2 和 c3 的值。

您可能会问,是否有任何函数具有闭包,甚至是在全局范围内创建的函数?答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。

当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。

闭包不是那么简单

有时候闭包在你甚至没有注意到它的时候就会出现,你可能已经看到了我们称为部分应用程序的示例,如下面的代码所示:

let c =4;            
const addX =x => n => n + x;
const addThree = addX(3);
let d = addThree(c);
console.log("example partial application", d);

如果箭头函数让您感到困惑,下面是同样效果:

let c =4;            
functionaddX(x) {
returnfunction(n) {
return n + x;
};
}
const addThree = addX(3);
let d = addThree(c);
console.log("example partial application", d);

我们声明一个能用加法函数 addX,它接受一个参数(x)并返回另一个函数。返回的函数还接受一个参数并将其添加到变量 x中。

变量 x是闭包的一部分,当变量 addThree在本地上下文中声明时,它被分配一个函数定义和一个闭包,闭包包含变量 x。

所以当 addThree被调用并执行时,它可以从闭包中访问变量 x以及为参数传递变量 n并返回两者的和 7。

总结

我将永远记住闭包的方法是通过背包的类比。当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。

【OpenCV入门教程之十二】OpenCV边缘检测:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑 - 【浅墨的游戏编程Blog】毛星云(浅墨)的专栏 - CSDN博客

$
0
0

本篇文章中,我们将一起学习OpenCV中边缘检测的各种算子和滤波器——Canny算子,Sobel算子,Laplace算子以及Scharr滤波器。文章中包含了五个浅墨为大家准备的详细注释的博文配套源代码。在介绍四块知识点的时候分别一个,以及最后的综合示例中的一个。文章末尾提供配套源代码的下载。

依然是是放出一些程序运行截图吧:

 

 效果图看完,我们来唠唠嗑。


首先,需要说明的是,浅墨这篇文章最后的示例代码是采用两周前刚刚发布的2.4.9来书写的。里面的lib都已经改成了2.4.9版本的。如果大家需要运行的话,要么配置好2.4.9.要么把浅墨在工程中包含的末尾数字为249的各种lib改成之前的248或者你对应的OpenCV版本。

不然会提示: LINK : fatal error LNK1181: 无法打开输入文件“opencv_calib3d248.lib”之类的错误。

OpenCV 2.4.9的配置和之前的2.4.8差不多,如果还是不太清楚,具体可以参考浅墨修改过的对应2.4.9版的配置文章:


【OpenCV入门教程之一】 安装OpenCV:OpenCV 2.4.8或2.4.9 +VS 开发环境配置

 

第二,给大家分享一个OpenCV中写代码时节约时间的小常识。其实OpenCV中,不用namedWindow,直接imshow就可以显示出窗口。大家看下文的示例代码就可以发现,浅墨在写代码的时候并没有用namedWindow,遇到想显示出来的Mat变量直接imshow。我们一般是为了规范,才先用namedWindow创建窗口,再imshow出它来,因为我们还有需要用到指定窗口名称的地方,比如用到trackbar的时候。而一般情况想显示一个Mat变量的图片的话,直接imshow就可以啦。

 

OK,开始正文吧~


 




一、关于边缘检测

       



在具体介绍之前,先来一起看看边缘检测的一般步骤吧。


1)滤波:边缘检测的算法主要是基于图像强度的一阶和二阶导数,但导数通常对噪声很敏感,因此必须采用滤波器来改善与噪声有关的边缘检测器的性能。常见的滤波方法主要有高斯滤波,即采用离散化的高斯函数产生一组归一化的高斯核(具体见“高斯滤波原理及其编程离散化实现方法”一文),然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和(具体程序实现见下文)。

 

       2)增强:增强边缘的基础是确定图像各点邻域强度的变化值。增强算法可以将图像灰度点邻域强度值有显著变化的点凸显出来。在具体编程实现时,可通过计算梯度幅值来确定。

 

       3)检测:经过增强的图像,往往邻域中有很多点的梯度值比较大,而在特定的应用中,这些点并不是我们要找的边缘点,所以应该采用某种方法来对这些点进行取舍。实际工程中,常用的方法是通过阈值化方法来检测。


另外,需要注意,下文中讲到的Laplace算子,sobel算子和Scharr算子都是带方向的,所以,示例中我们分别写了X方向,Y方向和最终合成的的效果图。


OK,正餐开始,召唤canny算子。:)

 








二、canny算子篇

 



2.1canny算子相关理论与概念讲解



2.1.1canny算子简介


Canny边缘检测算子是John F.Canny于 1986 年开发出来的一个多级边缘检测算法。更为重要的是 Canny 创立了边缘检测计算理论(Computational theory ofedge detection),解释了这项技术是如何工作的。Canny边缘检测算法以Canny的名字命名,被很多人推崇为当今最优的边缘检测的算法。

其中,Canny 的目标是找到一个最优的边缘检测算法,让我们看一下最优边缘检测的三个主要评价标准:

 

1.低错误率:标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。


2.高定位性:标识出的边缘要与图像中的实际边缘尽可能接近。


3.最小响应:图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。

 

为了满足这些要求 Canny 使用了变分法,这是一种寻找满足特定功能的函数的方法。最优检测使用四个指数函数项的和表示,但是它非常近似于高斯函数的一阶导数。

 



 

2.1.2Canny 边缘检测的步骤



1.消除噪声。一般情况下,使用高斯平滑滤波器卷积降噪。 如下显示了一个 size = 5 的高斯内核示例:




 

2.计算梯度幅值和方向。此处,按照Sobel滤波器的步骤。

 

Ⅰ.运用一对卷积阵列 (分别作用于 x 和 y 方向):


 



Ⅱ.使用下列公式计算梯度幅值和方向:



 

梯度方向近似到四个可能角度之一(一般为0, 45, 90, 135)

 

3.非极大值抑制。这一步排除非边缘像素, 仅仅保留了一些细线条(候选边缘)。


 

4.滞后阈值。最后一步,Canny 使用了滞后阈值,滞后阈值需要两个阈值(高阈值和低阈值):

 

Ⅰ.如果某一像素位置的幅值超过 高 阈值, 该像素被保留为边缘像素。

Ⅱ.如果某一像素位置的幅值小于 低 阈值, 该像素被排除。

.如果某一像素位置的幅值在两个阈值之间,该像素仅仅在连接到一个高于 高 阈值的像素时被保留。


tips:对于Canny函数的使用,推荐的高低阈值比在2:1到3:1之间。

 

更多的细节,可以参考canny算子的wikipedia:

http://en.wikipedia.org/wiki/Canny_edge_detector

canny边缘检测的原理讲述,课参看这篇博文:

http://blog.csdn.net/likezhaobin/article/details/6892176

canny算子的中文wikipedia:

http://zh.wikipedia.org/wiki/Canny%E7%AE%97%E5%AD%90

 



2.2OpenCV中Canny函数详解

 

Canny函数利用Canny算法来进行图像的边缘检测。

 

C++: void Canny(InputArray image,OutputArray edges, double threshold1, double threshold2, int apertureSize=3,bool L2gradient=false )


  • 第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
  • 第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和类型。
  • 第三个参数,double类型的threshold1,第一个滞后性阈值。
  • 第四个参数,double类型的threshold2,第二个滞后性阈值。
  • 第五个参数,int类型的apertureSize,表示应用Sobel算子的孔径大小,其有默认值3。
  • 第六个参数,bool类型的L2gradient,一个计算图像梯度幅值的标识,有默认值false。

 

需要注意的是,这个函数阈值1和阈值2两者的小者用于边缘连接,而大者用来控制强边缘的初始段,推荐的高低阈值比在2:1到3:1之间。

 

调用示例:

//载入原始图 
       Mat src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
       Canny(src, src, 3, 9,3 );
       imshow("【效果图】Canny边缘检测", src);

如上三句,就有结果出来,非常好用。

 

 



2.3调用Canny函数的实例代码

 


OpenCV中调用Canny函数的实例代码如下:

 

//-----------------------------------【头文件包含部分】---------------------------------------
//            描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
//            描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//载入原始图  
	Mat src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图
	Mat src1=src.clone();

	//显示原始图 
	imshow("【原始图】Canny边缘检测", src); 

	//----------------------------------------------------------------------------------
	//	一、最简单的canny用法,拿到原图后直接用。
	//----------------------------------------------------------------------------------
	Canny( src, src, 150, 100,3 );
	imshow("【效果图】Canny边缘检测", src); 

	
	//----------------------------------------------------------------------------------
	//	二、高阶的canny用法,转成灰度图,降噪,用canny,最后将得到的边缘作为掩码,拷贝原图到效果图上,得到彩色的边缘图
	//----------------------------------------------------------------------------------
	Mat dst,edge,gray;

	// 【1】创建与src同类型和大小的矩阵(dst)
	dst.create( src1.size(), src1.type() );

	// 【2】将原图像转换为灰度图像
	cvtColor( src1, gray, CV_BGR2GRAY );

	// 【3】先用使用 3x3内核来降噪
	blur( gray, edge, Size(3,3) );

	// 【4】运行Canny算子
	Canny( edge, edge, 3, 9,3 );

	//【5】将g_dstImage内的所有元素设置为0 
	dst = Scalar::all(0);

	//【6】使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码,来将原图g_srcImage拷到目标图g_dstImage中
	src1.copyTo( dst, edge);

	//【7】显示效果图 
	imshow("【效果图】Canny边缘检测2", dst); 


	waitKey(0); 

	return 0; 
}


运行效果图:

 


 

 

 










三、sobel算子篇





3.1sobel算子相关理论与概念讲解




3.1.1基本概念


Sobel 算子是一个主要用作边缘检测的离散微分算子 (discrete differentiation operator)。 它Sobel算子结合了高斯平滑和微分求导,用来计算图像灰度函数的近似梯度。在图像的任何一点使用此算子,将会产生对应的梯度矢量或是其法矢量。


sobel算子的wikipedia:

http://zh.wikipedia.org/wiki/%E7%B4%A2%E8%B2%9D%E7%88%BE%E7%AE%97%E5%AD%90

 

sobel算子相关概念,还可以参看这篇博文:

http://www.cnblogs.com/lancidie/archive/2011/07/17/2108885.html

 



3.1.2sobel算子的计算过程



我们假设被作用图像为 I.然后进行如下的操作:

 

1.分别在x和y两个方向求导。


Ⅰ.水平变化:将 I 与一个奇数大小的内核 进行卷积。比如,当内核大小为3时, 的计算结果为:


 

 

 

Ⅱ.垂直变化:将: I 与一个奇数大小的内核 进行卷积。比如,当内核大小为3时, 的计算结果为:


 

 

2.在图像的每一点,结合以上两个结果求出近似梯度:

 


另外有时,也可用下面更简单公式代替:

 

 

 


3.2OpenCV中Sobel函数详解



Sobel函数使用扩展的 Sobel 算子,来计算一阶、二阶、三阶或混合图像差分。


C++: void Sobel (
InputArray src,//输入图
 OutputArray dst,//输出图
 int ddepth,//输出图像的深度
 int dx,
 int dy,
 int ksize=3,
 double scale=1,
 double delta=0,
 int borderType=BORDER_DEFAULT );


  • 第一个参数,InputArray 类型的src,为输入图像,填Mat类型即可。
  • 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。
  • 第三个参数,int类型的ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合:
    • 若src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F
    • 若src.depth() = CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F
    • 若src.depth() = CV_32F, 取ddepth =-1/CV_32F/CV_64F
    • 若src.depth() = CV_64F, 取ddepth = -1/CV_64F
  • 第四个参数,int类型dx,x 方向上的差分阶数。
  • 第五个参数,int类型dy,y方向上的差分阶数。
  • 第六个参数,int类型ksize,有默认值3,表示Sobel核的大小;必须取1,3,5或7。
  • 第七个参数,double类型的scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍,来得到这个参数的更多信息。
  • 第八个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
  • 第九个参数, int类型的borderType,我们的老朋友了(万年是最后一个参数),边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。


一般情况下,都是用ksize x ksize内核来计算导数的。然而,有一种特殊情况——当ksize为1时,往往会使用3 x 1或者1 x 3的内核。且这种情况下,并没有进行高斯平滑操作。

 

一些补充说明:


1.当内核大小为 3 时, 我们的Sobel内核可能产生比较明显的误差(毕竟,Sobel算子只是求取了导数的近似值而已)。 为解决这一问题,OpenCV提供了Scharr 函数,但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快,但结果却更加精确,其内核是这样的:


 

 

2.因为Sobel算子结合了高斯平滑和分化(differentiation),因此结果会具有更多的抗噪性。大多数情况下,我们使用sobel函数时,取【xorder = 1,yorder = 0,ksize = 3】来计算图像X方向的导数,【xorder = 0,yorder = 1,ksize = 3】来计算图像y方向的导数。

计算图像X方向的导数,取【xorder= 1,yorder = 0,ksize = 3】情况对应的内核:


 

 

而计算图像Y方向的导数,取【xorder= 0,yorder = 1,ksize = 3】对应的内核:



 


3.3调用Sobel函数的实例代码



调用Sobel函数的实例代码如下。这里只是教大家如何使用Sobel函数,就没有先用一句cvtColor将原图;转化为灰度图,而是直接用彩色图操作。

 

//-----------------------------------【头文件包含部分】---------------------------------------
//            描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
//            描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//【0】创建 grad_x 和 grad_y 矩阵
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y,dst;

	//【1】载入原始图  
	Mat src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow("【原始图】sobel边缘检测", src); 

	//【3】求 X方向梯度
	Sobel( src, grad_x, CV_16S, 1, 0, 3, 1, 1, BORDER_DEFAULT );
	convertScaleAbs( grad_x, abs_grad_x );
	imshow("【效果图】 X方向Sobel", abs_grad_x); 

	//【4】求Y方向梯度
	Sobel( src, grad_y, CV_16S, 0, 1, 3, 1, 1, BORDER_DEFAULT );
	convertScaleAbs( grad_y, abs_grad_y );
	imshow("【效果图】Y方向Sobel", abs_grad_y); 

	//【5】合并梯度(近似)
	addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst );
	imshow("【效果图】整体方向Sobel", dst); 

	waitKey(0); 
	return 0; 
}


运行截图如下:

 


 











四、Laplace算子篇




4.1Laplace算子相关理论与概念讲解

 

Laplacian 算子是n维欧几里德空间中的一个二阶微分算子,定义为梯度grad()的散度div()。因此如果f是二阶可微的实函数,则f的拉普拉斯算子定义为:


(1)f的拉普拉斯算子也是笛卡儿坐标系xi中的所有非混合二阶偏导数求和:

(2)作为一个二阶微分算子,拉普拉斯算子把C函数映射到C函数,对于k ≥ 2。表达式(1)(或(2))定义了一个算子Δ :C(R) → C(R),或更一般地,定义了一个算子Δ : C(Ω) → C(Ω),对于任何开集Ω。

 

根据图像处理的原理我们知道,二阶导数可以用来进行检测边缘 。 因为图像是 “二维”, 我们需要在两个方向进行求导。使用Laplacian算子将会使求导过程变得简单。

 

Laplacian 算子的定义:


 

 

需要点破的是,由于 Laplacian使用了图像梯度,它内部的代码其实是调用了 Sobel 算子的。


另附一个小tips:让一幅图像减去它的Laplacian可以增强对比度。


 

关于Laplace算子的相关概念阐述,可以参看这篇博文:

http://www.cnblogs.com/xfzhang/archive/2011/01/19/1939020.html

Laplace算子的wikipedia:

http://zh.wikipedia.org/wiki/%E6%8B%89%E6%99%AE%E6%8B%89%E6%96%AF%E7%AE%97%E5%AD%90

 



4.2OpenCV中Laplacian函数详解



Laplacian函数可以计算出图像经过拉普拉斯变换后的结果。

 

C++: void Laplacian(InputArray src,OutputArray dst, int ddepth, int ksize=1, double scale=1, double delta=0, intborderType=BORDER_DEFAULT );

  • 第一个参数,InputArray类型的image,输入图像,即源图像,填Mat类的对象即可,且需为单通道8位图像。
  • 第二个参数,OutputArray类型的edges,输出的边缘图,需要和源图片有一样的尺寸和通道数。
  • 第三个参数,int类型的ddept,目标图像的深度。
  • 第四个参数,int类型的ksize,用于计算二阶导数的滤波器的孔径尺寸,大小必须为正奇数,且有默认值1。
  • 第五个参数,double类型的scale,计算拉普拉斯值的时候可选的比例因子,有默认值1。
  • 第六个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
  • 第七个参数, int类型的borderType,边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate()处得到更详细的信息。


Laplacian( )函数其实主要是利用sobel算子的运算。它通过加上sobel算子运算出的图像x方向和y方向上的导数,来得到我们载入图像的拉普拉斯变换结果。

其中,sobel算子(ksize>1)如下:


 

而当ksize=1时,Laplacian()函数采用以下3x3的孔径:


 

 

 

 

4.3调用Laplacian函数的实例代码

 

 

让我们看一看调用实例:

 

//-----------------------------------【头文件包含部分】---------------------------------------
//            描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;


//-----------------------------------【main( )函数】--------------------------------------------
//            描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//【0】变量的定义
	Mat src,src_gray,dst, abs_dst;

	//【1】载入原始图  
	src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow("【原始图】图像Laplace变换", src); 

	//【3】使用高斯滤波消除噪声
	GaussianBlur( src, src, Size(3,3), 0, 0, BORDER_DEFAULT );

	//【4】转换为灰度图
	cvtColor( src, src_gray, CV_RGB2GRAY );

	//【5】使用Laplace函数
	Laplacian( src_gray, dst, CV_16S, 3, 1, 0, BORDER_DEFAULT );

	//【6】计算绝对值,并将结果转换成8位
	convertScaleAbs( dst, abs_dst );

	//【7】显示效果图
	imshow( "【效果图】图像Laplace变换", abs_dst );

	waitKey(0); 

	return 0; 
}

示例效果图:

 

 

 

 

 










五、scharr滤波器篇



scharr一般我就直接称它为滤波器,而不是算子。上文我们已经讲到,它在OpenCV中主要是配合Sobel算子的运算而存在的,一个万年备胎。让我们直接来看看函数讲解吧。



 

5.1OpenCV中Scharr函数详解

 


使用Scharr滤波器运算符计算x或y方向的图像差分。其实它的参数变量和Sobel基本上是一样的,除了没有ksize核的大小。

 

C++: void Scharr(
InputArray src, //源图
 OutputArray dst, //目标图
 int ddepth,//图像深度
 int dx,// x方向上的差分阶数
 int dy,//y方向上的差分阶数
 double scale=1,//缩放因子
 double delta=0,// delta值
 intborderType=BORDER_DEFAULT )// 边界模式


  • 第一个参数,InputArray 类型的src,为输入图像,填Mat类型即可。
  • 第二个参数,OutputArray类型的dst,即目标图像,函数的输出参数,需要和源图片有一样的尺寸和类型。
  • 第三个参数,int类型的ddepth,输出图像的深度,支持如下src.depth()和ddepth的组合:
    • 若src.depth() = CV_8U, 取ddepth =-1/CV_16S/CV_32F/CV_64F
    • 若src.depth() = CV_16U/CV_16S, 取ddepth =-1/CV_32F/CV_64F
    • 若src.depth() = CV_32F, 取ddepth =-1/CV_32F/CV_64F
    • 若src.depth() = CV_64F, 取ddepth = -1/CV_64F
  • 第四个参数,int类型dx,x方向上的差分阶数。
  • 第五个参数,int类型dy,y方向上的差分阶数。
  • 第六个参数,double类型的scale,计算导数值时可选的缩放因子,默认值是1,表示默认情况下是没有应用缩放的。我们可以在文档中查阅getDerivKernels的相关介绍,来得到这个参数的更多信息。
  • 第七个参数,double类型的delta,表示在结果存入目标图(第二个参数dst)之前可选的delta值,有默认值0。
  • 第八个参数, int类型的borderType,我们的老朋友了(万年是最后一个参数),边界模式,默认值为BORDER_DEFAULT。这个参数可以在官方文档中borderInterpolate处得到更详细的信息。

 

不难理解,如下两者是等价的:

Scharr(src, dst, ddepth, dx, dy, scale,delta, borderType);

Sobel(src, dst, ddepth, dx, dy, CV_SCHARR,scale, delta, borderType);



 

5.2调用Scharr函数的实例代码

 


//-----------------------------------【头文件包含部分】---------------------------------------
//            描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>

//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
//-----------------------------------【main( )函数】--------------------------------------------
//            描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
	//【0】创建 grad_x 和 grad_y 矩阵
	Mat grad_x, grad_y;
	Mat abs_grad_x, abs_grad_y,dst;

	//【1】载入原始图  
	Mat src = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图

	//【2】显示原始图 
	imshow("【原始图】Scharr滤波器", src); 

	//【3】求 X方向梯度
	Scharr( src, grad_x, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( grad_x, abs_grad_x );
	imshow("【效果图】 X方向Scharr", abs_grad_x); 

	//【4】求Y方向梯度
	Scharr( src, grad_y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( grad_y, abs_grad_y );
	imshow("【效果图】Y方向Scharr", abs_grad_y); 

	//【5】合并梯度(近似)
	addWeighted( abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dst );

	//【6】显示效果图
	imshow("【效果图】合并梯度后Scharr", dst); 

	waitKey(0); 
	return 0; 
}


运行效果图:

 




 





 


六、综合示例篇——在实战中熟稔

 


 

依然是每篇文章都会配给大家的一个详细注释的博文配套示例程序,把这篇文章中介绍的知识点以代码为载体,展现给大家。

这个示例程序中,分别演示了canny边缘检测,sobel边缘检测,scharr滤波器的使用,那么,上详细注释的代码吧:

 

//-----------------------------------【程序说明】----------------------------------------------
//		程序名称::《【OpenCV入门教程之十二】OpenCV边缘检测:Canny算子,Sobel算子,Laplace算子,Scharr滤波器合辑合辑》 博文配套源码 
//		开发所用IDE版本:Visual Studio 2010
//开发所用OpenCV版本:	2.4.9
//		2014年5月11日 Create by 浅墨
//		浅墨的微博:@浅墨_毛星云 http://weibo.com/1723155442/profile?topnav=1&wvr=5&user=1
//		浅墨的知乎:http://www.zhihu.com/people/mao-xing-yun
//		浅墨的豆瓣:http://www.douban.com/people/53426472/
//----------------------------------------------------------------------------------------------



//-----------------------------------【头文件包含部分】---------------------------------------
//		描述:包含程序所依赖的头文件
//---------------------------------------------------------------------------------------------- 
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

//-----------------------------------【命名空间声明部分】--------------------------------------
//		描述:包含程序所使用的命名空间
//----------------------------------------------------------------------------------------------- 
using namespace cv;


//-----------------------------------【全局变量声明部分】--------------------------------------
//		描述:全局变量声明
//-----------------------------------------------------------------------------------------------
//原图,原图的灰度版,目标图
Mat g_srcImage, g_srcGrayImage,g_dstImage;

//Canny边缘检测相关变量
Mat g_cannyDetectedEdges;
int g_cannyLowThreshold=1;//TrackBar位置参数  

//Sobel边缘检测相关变量
Mat g_sobelGradient_X, g_sobelGradient_Y;
Mat g_sobelAbsGradient_X, g_sobelAbsGradient_Y;
int g_sobelKernelSize=1;//TrackBar位置参数  

//Scharr滤波器相关变量
Mat g_scharrGradient_X, g_scharrGradient_Y;
Mat g_scharrAbsGradient_X, g_scharrAbsGradient_Y;


//-----------------------------------【全局函数声明部分】--------------------------------------
//		描述:全局函数声明
//-----------------------------------------------------------------------------------------------
static void ShowHelpText( );
static void on_Canny(int, void*);//Canny边缘检测窗口滚动条的回调函数
static void on_Sobel(int, void*);//Sobel边缘检测窗口滚动条的回调函数
void Scharr( );//封装了Scharr边缘检测相关代码的函数


//-----------------------------------【main( )函数】--------------------------------------------
//		描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( int argc, char** argv )
{
	//改变console字体颜色
	system("color 2F");  

	//显示欢迎语
	ShowHelpText();

	//载入原图
	g_srcImage = imread("1.jpg");
	if( !g_srcImage.data ) { printf("Oh,no,读取srcImage错误~! \n"); return false; }

	//显示原始图
	namedWindow("【原始图】");
	imshow("【原始图】", g_srcImage);

	// 创建与src同类型和大小的矩阵(dst)
	g_dstImage.create( g_srcImage.size(), g_srcImage.type() );

	// 将原图像转换为灰度图像
	cvtColor( g_srcImage, g_srcGrayImage, CV_BGR2GRAY );

	// 创建显示窗口
	namedWindow( "【效果图】Canny边缘检测", CV_WINDOW_AUTOSIZE );
	namedWindow( "【效果图】Sobel边缘检测", CV_WINDOW_AUTOSIZE );

	// 创建trackbar
	createTrackbar( "参数值:", "【效果图】Canny边缘检测", &g_cannyLowThreshold, 120, on_Canny );
	createTrackbar( "参数值:", "【效果图】Sobel边缘检测", &g_sobelKernelSize, 3, on_Sobel );

	// 调用回调函数
	on_Canny(0, 0);
	on_Sobel(0, 0);

	//调用封装了Scharr边缘检测代码的函数
	Scharr( );

	//轮询获取按键信息,若按下Q,程序退出
	while((char(waitKey(1)) != 'q')) {}

	return 0;
}


//-----------------------------------【ShowHelpText( )函数】----------------------------------
//		描述:输出一些帮助信息
//----------------------------------------------------------------------------------------------
static void ShowHelpText()
{
	//输出一些帮助信息
	printf( "\n\n\t嗯。运行成功,请调整滚动条观察图像效果~\n\n""\t按下“q”键时,程序退出~!\n""\n\n\t\t\t\t by浅墨"	);
}


//-----------------------------------【on_Canny( )函数】----------------------------------
//		描述:Canny边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------------
void on_Canny(int, void*)
{
	// 先使用 3x3内核来降噪
	blur( g_srcGrayImage, g_cannyDetectedEdges, Size(3,3) );

	// 运行我们的Canny算子
	Canny( g_cannyDetectedEdges, g_cannyDetectedEdges, g_cannyLowThreshold, g_cannyLowThreshold*3, 3 );

	//先将g_dstImage内的所有元素设置为0 
	g_dstImage = Scalar::all(0);

	//使用Canny算子输出的边缘图g_cannyDetectedEdges作为掩码,来将原图g_srcImage拷到目标图g_dstImage中
	g_srcImage.copyTo( g_dstImage, g_cannyDetectedEdges);

	//显示效果图
	imshow( "【效果图】Canny边缘检测", g_dstImage );
}



//-----------------------------------【on_Sobel( )函数】----------------------------------
//		描述:Sobel边缘检测窗口滚动条的回调函数
//-----------------------------------------------------------------------------------------
void on_Sobel(int, void*)
{
	// 求 X方向梯度
	Sobel( g_srcImage, g_sobelGradient_X, CV_16S, 1, 0, (2*g_sobelKernelSize+1), 1, 1, BORDER_DEFAULT );
	convertScaleAbs( g_sobelGradient_X, g_sobelAbsGradient_X );//计算绝对值,并将结果转换成8位

	// 求Y方向梯度
	Sobel( g_srcImage, g_sobelGradient_Y, CV_16S, 0, 1, (2*g_sobelKernelSize+1), 1, 1, BORDER_DEFAULT );
	convertScaleAbs( g_sobelGradient_Y, g_sobelAbsGradient_Y );//计算绝对值,并将结果转换成8位

	// 合并梯度
	addWeighted( g_sobelAbsGradient_X, 0.5, g_sobelAbsGradient_Y, 0.5, 0, g_dstImage );

	//显示效果图
	imshow("【效果图】Sobel边缘检测", g_dstImage); 

}


//-----------------------------------【Scharr( )函数】----------------------------------
//		描述:封装了Scharr边缘检测相关代码的函数
//-----------------------------------------------------------------------------------------
void Scharr( )
{
	// 求 X方向梯度
	Scharr( g_srcImage, g_scharrGradient_X, CV_16S, 1, 0, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( g_scharrGradient_X, g_scharrAbsGradient_X );//计算绝对值,并将结果转换成8位

	// 求Y方向梯度
	Scharr( g_srcImage, g_scharrGradient_Y, CV_16S, 0, 1, 1, 0, BORDER_DEFAULT );
	convertScaleAbs( g_scharrGradient_Y, g_scharrAbsGradient_Y );//计算绝对值,并将结果转换成8位

	// 合并梯度
	addWeighted( g_scharrAbsGradient_X, 0.5, g_scharrAbsGradient_Y, 0.5, 0, g_dstImage );

	//显示效果图
	imshow("【效果图】Scharr滤波器", g_dstImage); 
}


放出一些运行效果图:

 

canny边缘检测效果图:



 


Sobel边缘检测:



Scharr滤波器:



好的,就放出这些效果图吧,具体更多的运行效果大家就自己下载示例程序回去玩~

 

本篇文章的配套源代码请点击这里下载:

 

【浅墨OpenCV入门教程之十二】配套源代码下载

 

 

OK,今天的内容大概就是这些,我们下篇文章见:)

 

 

 

人脸检测、人脸对齐(MTCNN方法) - 花泽 - CSDN博客

$
0
0

众所众知,严格定义上的人脸识别分为四个步骤:

①人脸检测:从图片中准确定位到人脸

②人脸矫正(对齐): 检测到的人脸,可能角度不是很正,需要使其对齐

③对矫正后的人脸进行特征提取

④对两张人脸图像的特征向量进行对比,计算相似度

这里,我们主要是推荐步骤1和步骤2用到的一个方法,论文是

《2016 Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks》.  这篇论文具体的思想大家可以自己阅读。

称之为MTCNN人脸检测算法,同时有大神已经GitHub上开源了其基于caffe的C++ API 的源代码, https://github.com/DaFuCoding/MTCNN_Caffe  再次感谢大神以及开源~

这个GitHub上是原始的caffe,再加上了MTCNN的部分(一个源文件+8个训练好的caffemodel以及deploy文件),大家可以clone到本地从头编译。编译完后,生成一个可执行文件,路径为 build/examples/MTSrc/MTMain.bin ,这个可执行文件需要额外的两个参数,第一个是上面的8个文件所在目录(caffe/examples/MTmodel/),第二参数就是测试图片了。这里我们先运行下demo,将一张图片放到caffe目录下(放哪都行),然后转到caffe根目录下,运行 ./build/examples/MTSrc/MTMain.bin /examples/MTmodel/ 1.jpg 然后很快就出结果了,检测到人脸,以及五个特征点(两只眼睛,鼻子,左嘴角和右嘴角),具体demo展示可看GitHub的步骤。

                                                                 

同时打印出图片尺寸信息,以及检测耗时。

这个接口一次性能检测一张图片,以及给出每个人脸的五个特征点,而我们除了要得到人脸外,还要进行人脸对齐,只要将对齐后的人脸送进网络才能得到更高的精度,所以,为了满足这个要求,我对源文件MTMain.cpp进行了修改,分为了头文件以及源文件,并且添加了一些代码,得到人脸框以及五个特征点后,通道OpenCV里的仿射变换,使之对齐,效果如下                      

                                                    

左边的是输入图片,中间是经过人脸检测的图像,右边的是对齐后的图像,效果还是很好的。

我把MTMain.cpp文件进行修改后,分为了两个文件 MTCNN.h和MTCNN.cpp , 修改后的代码中,只开放了一个接口函数,调用十分简单,如下:

//核心代码在这
class MTCNN{
public:
       MTCNN(const string& proto_model_dir);   //构造函数,参数是8个caffemode、deploy文件所在目录路径
       
       //给定一张图片,faceRects用于保存检测到的人脸框,函数返回所有对齐后的人脸图片
       vector<Mat> getFaceRects(const Mat& img,vector<Rect>* faceRects=NULL);略.........
对一张图片,只要不断的调用该函数,就可以返回对齐后的人脸了,注意这里返回值是个图片数组,因为可能一张图中有多个人脸,所以返回的也是多个人脸。

有了这个方便的接口后,大家根据自己的需求,写个main.cpp 文件,把程序跑起来吧~~

这两个文件的百度云链接:链接: http://pan.baidu.com/s/1pLBGwuZ密码:fcyc

对了,也许有些小伙伴,不知道怎么单独编译这两个文件生成可执行文件,这里提供个样例Makefile文件,实际路径换上自己的,make一下就好了~

# Makefile

main.bin: main.cpp MTCNN.cpp
      g++ -o main.bin main.cpp MTCNN.cpp -I /home/yourname/caffe/include -I /home/yourname/caffe/build/src/ -I /usr/local/cuda/include -L /home/yourname/caffe/build/lib -lcaffe -lglog /-boost_system -lprotobuf `pkg-config --libs --cflags opencv`

当然,如果只有CPU,可以把cuda的那个删去,同时记得在最后加一句 -D CPU_ONLY

最后,分享下用opencv进行仿射变换的博客

http://blog.csdn.net/u013713010/article/details/46047367


好了,接下来,自己跑起来吧~



人脸对齐(一)--定义及作用 - 工作笔记 - CSDN博客

$
0
0

参考:

http://www.thinkface.cn/thread-4354-1-1.html

http://www.thinkface.cn/thread-4488-1-1.html

人脸对齐任务即根据输入的人脸图像,自动定位出面部关键特征点,如眼睛、鼻尖、嘴角点、眉毛以及人脸各部件轮廓点等,如下图所示。

 

     这项技术的应用很广泛,比如自动人脸识别,表情识别以及人脸动画自动合成等。由于不同的姿态、表情、光照以及遮挡等因素的影响,准确地定位出各个关键特征点看似很困难。我们简单地分析一下这个问题,不难发现这个任务其实可以拆分出三个子问题:
    1. 如何对人脸表观图像(输入)建模
    2. 如何对人脸形状(输出)建模
    3.如何建立人脸表观图像(模型)与人脸形状(模型)的关联
    以往的研究工作也离不开这三个方面。人脸形状建模典型的方法有可变形模板(Deformable Template)、点分布模型(主动形状模型Active Shape Model)、图模型等。
   人脸表观建模又可分为全局表观建模和局部表观建模。全局表观建模简单的说就是考虑如何建模整张人脸的表观信息,典型的方法有主动表观模型Active Appearance Model(产生式模型)和Boosted Appearance Model(判别式模型)。对应的局部表观建模则是对局部区域的表观信息建模,包括颜色模型、投影模型、侧剖线模型等。
    近来,级联形状回归模型在特征点定位任务上取得了重大突破,该方法使用回归模型,直接学习从人脸表观到人脸形状(或者人脸形状模型的参数)的映射函数,进而建立从表观到形状的对应关系。此类方法不需要复杂的人脸形状和表观建模,简单高效,在可控场景(实验室条件下采集的人脸)和非可控场景(网络人脸图像等)均取得不错的定位效果。此外,基于深度学习的人脸对齐方法也取得令人瞩目的结果。深度学习结合形状回归框架可以进一步提升定位模型的精度,成为当前特征定位的主流方法之一。下面将具体介绍级联形状回归和深度学习这两大类方法的研究进展。
级联线性回归模型
    人脸对齐问题可以看作是学习一个回归函数F,以图象I作为输入,输出θ为特征点的位置(人脸形状):θ = F(I)。
    简单的说,级联回归模型可以统一为以下框架:学习多个回归函数{f1 ,…, fn-1, fn}来逼近函数F:

θ = F(I)=  fn (fn-1 (…f1(θ0, I) ,I) , I)

θi= fi (θi-1, I),    i=1,…,n

    所谓的级联,即当前函数fi的输入依赖于上一级函数fi-1的输出θi-1,而每一个fi的学习目标都是逼近特征点的真实位置θ,θ0为初始形状。通常情况,fi不是直接回归真实位置θ,而回归当前形状θi-1与真实位置θ之间的差:Δθi = θ - θi-1。
    接下来我将详细介绍几个典型的形状回归方法,他们根本的不同点在于函数fi的设计不同以及输入特征不同。
    在加州理工学院从事博士后研究的Piotr Dollár于2010年首次提出级联形状回归模型CascadedPose Regression(CPR),来预测物体的形状,该工作发表在国际计算机视觉与模式识别会议CVPR上。如下图所示,如下图所示,给定初始形状θ0,通常为平均形状,根据初始形状θ0提取特征(两个像素点的差值)作为函数f1的输入。每个函数fi建模成Random Fern回归器,来预测当前形状θi-1与目标形状θ的差Δθi,并根据Δθi预测结果更新当前形状得θ i = θi-1+Δθi,作为下一级函数fi+1的输入。该方法在人脸、老鼠和鱼三个数据集上取得不错的实验结果,通用的算法框架亦可用于其他形状估计任务,比如人体姿态估计等。该方法的不足之处在于对初始化形状θ0比较敏感,使用不同的初始化做多次测试并融合多次预测结果可以一定程度上缓解初始化对于算法的影响,但并不能完全解决该问题,且多次测试会带来额外的运算开销。当目标物体被遮挡时,性能也会变差。

 


    与上一个工作来自同一课题组的Xavier P. Burgos-Artizzu,针对CPR方法的不足,进一步提出Robust Cascaded Pose Regression(RCPR)方法,并发表在2013年国际计算视觉会议ICCV上。为了解决遮挡问题,Piotr Dollár提出同时预测人脸形状和特征点是否被遮挡的状态,即fi的输出包含Δθi和每个特征点是否被遮挡的状态pi:

{Δθi , pi }= fi(θi-1, I),    i=1,…,n

    当某些特征点被遮挡时,则不选取该特征点所在区域的特征作为输入,从而避免遮挡对定位的干扰。此外,作者提出智能重启技术来解决形状初始化敏感的问题:随机初始化一组形状,运行{f1 ,…,fn-1, fn}的前10%的函数,统计形状预测的方差,如果方差小于一定阈值,说明这组初始化不错,则跑完剩下的90%的级联函数,得到最终的预测结果;如果方差大于一定阈值,则说明初始化不理想,选择重新初始化一组形状。该策略想法直接,但效果很不错。
    另外一个很有趣的工作Supervised Descent Method(SDM),从另一个角度思考问题,即考虑如何使用监督梯度下降的方法来求解非线性最小二乘问题,并成功地应用在人脸对齐任务上。不难发现,该方法最终的算法框架也是一个级联回归模型。与CPR和RCPR不同的地方在于:fi建模成了线性回归模型;fi的输入为与人脸形状相关的SIFT特征。该特征的提取也很简单,即在当前人脸形状θi-1的每个特征点上提取一个128维的SIFT特征,并将所有SIFT特征串联到一起作为fi的输入。该方法在LFPW和LFW-A&C数据集上取得不错的定位结果。同时期的另一个工作DRMF则是使用支持向量回归SVR来建模回归函数fi,并使用形状相关的HOG特征(提取方式与形状相关的SIFT类似)作为fi输入,来级联预测人脸形状。与SDM最大的不同在于,DRMF对于人脸形状做了参数化的建模。fi的目标变为预测这些形状参数而不再是直接的人脸形状。这两个工作同时发表在CVPR 2013上。由于人脸形状参数化模型很难完美地刻画所有形状变化,SDM的实测效果要优于DRMF。
    微软亚洲研究院孙剑研究员的团队在CVPR 2014上提出更加高效的级联形状回归方法Regressing LocalBinary Features(LBF)。和SDM类似,fi也是建模成线性回归模型;不同的地方在于,SDM直接使用SIFT特征,LBF则基于随机森林回归模型在局部区域学习稀疏二值化特征。通过学习稀疏二值化特征,大大减少了运算开销,比CRP、RCPR、SDM、DRMF等方法具有更高的运行效率(LBF可以在手机上跑到300FPS),并且在IBUG公开评测集上取得优于SDM、RCPR的性能。

    级联形状回归模型成功的关键在于:
    1. 使用了形状相关特征,即函数fi的输入和当前的人脸形状θi-1紧密相关;
    2. 函数fi的目标也与当前的人脸形状θi-1相关,即fi的优化目标为当前形状θi-1与真实位置θ之间的差Δθi。
    此类方法在可控和非可控的场景下均取得良好的定位效果,且具有很好的实时性。
深度模型
    以上介绍的级联形状回归方法每一个回归函数fi都是浅层模型(线性回归模型、Random Fern等)。深度网络模型,比如卷积神经网络(CNN)、深度自编码器(DAE)和受限玻尔兹曼机(RBM)在计算机视觉的诸多问题,如场景分类,目标跟踪,图像分割等任务中有着广泛的应用,当然也包括特征定位问题。具体的方法可以分为两大类:使用深度模型建模人脸形状和表观的变化和基于深度网络学习从人脸表观到形状的非线性映射函数。
    主动形状模型ASM和主动表观模型AAM使用主成分分析(PCA)来建模人脸形状的变化。由于姿态表情等因素的影响,线性PCA模型很难完美地刻画不同表情和姿态下的人脸形状变化。来自伦斯勒理工学院JiQiang教授的课题组在CVPR2013提出使用深度置信网络(DBN)来刻画不同表情下人脸形状的复杂非线性变化。此外,为了处理不同姿态的特征点定位问题,进一步使用3向RBM网络建模从正面到非正面的人脸形状变化。最终该方法在表情数据库CK+上取得比线性模型AAM更好的定位结果。该方法在同时具备多姿态多表情的数据库
ISL上也取得较好的定位效果,但对同时出现极端姿态和夸张表情变化的情况还不够理想。
    下图是深度置信网络(DBN):建模不同表情下的人脸形状变化的示意图。

 

    香港中文大学唐晓鸥教授的课题组在CVPR 2013上提出3级卷积神经网络DCNN来实现人脸对齐的方法。该方法也可以统一在级联形状回归模型的大框架下,和CPR、RCPR、SDM、LBF等方法不一样的是,DCNN使用深度模型-卷积神经网络,来实现fi。第一级f1使用人脸图像的三块不同区域(整张人脸,眼睛和鼻子区域,鼻子和嘴唇区域)作为输入,分别训练3个卷积神经网络来预测特征点的位置,网络结构包含4个卷积层,3个Pooling层和2个全连接层,并融合三个网络的预测来得到更加稳定的定位结果。后面两级f2, f3在每个特征点附近抽取特征,针对每个特征点单独训练一个卷积神经网络(2个卷积层,2个Pooling层和1个全连接层)来修正定位的结果。该方法在LFPW数据集上取得当时最好的定位结果。

 

    另外一种由粗到精的自编码器网络(CFAN)来描述从人脸表观到人脸形状的复杂非线性映射过程。该方法级联了多个栈式自编码器网络fi,每一个fi刻画从人脸表观到人脸形状的部分非线性映射。具体来说,输入一个低分辨率的人脸图像I,第一层自编码器网络f1可以快速地估计大致的人脸形状,记作基于全局特征的栈式自编码网络。网络f1包含三个隐层,隐层节点数分别为1600,900,400。然后提高人脸图像的分辨率,并根据f1得到的初始人脸形状θ1,抽取联合局部特征,输入到下一层自编码器网络f2来同时优化、调整所有特征点的位置,记作基于局部特征的栈式自编码网络。该方法级联了3个局部栈式自编码网络{f2 , f3, f4}直到在训练集上收敛。每一个局部栈式自编码网络包含三个隐层,隐层节点数分别为1296,784,400。得益于深度模型强大的非线性刻画能力,该方法在XM2VTS,LFPW,HELEN数据集上取得比DRMF、SDM更好的结果。此外,CFAN可以实时地完成人脸人脸对齐(在I7的台式机上达到23毫秒/张),比DCNN(120毫秒/张)具有更快的处理速度。
    下图是CFAN:基于由粗到精自编码器网络的实时人脸对齐方法的示意图。

 

    以上基于级联形状回归和深度学习的方法对于大姿态(左右旋转-60°~+60°)、各种表情变化都能得到较好的定位结果,处理速度快,具备很好的产品应用前景。针对纯侧面(±90°)、部分遮挡以及人脸检测与特征定位联合估计等问题的解决仍是目前的研究热点。

目前有很多的人脸对齐算法,比较传统的有ASM、AAM、CLM和一些列改进算法,而目前比较流行的有ESR、3D-ESR、SPR、LBF、SDM、CFSS等。

ASM算法相对容易,其中STASM是目前正面脸当中比较好的算法,原作者和CLM比较过。但是STASM速度较慢,大概10frame/s左右。ASM对齐在精度上不如AAM,AAM由于使用全局纹理信息,因此精度较高,但是遇到光照和多姿态时,对初始化Shape要求很高,不然容易陷入局部优化。CLM分别继承了ASM和AAM的一些特征,效果得到了提升。对局部器官特征的概率假设和优化算法的选择,是CLM各种算法的本质区别。CLM比较好的论文和文献有:

automati feature localtion with constraint models  David Cristinacce, Tim Cootes

 

Face Alignment through Subspace ConstrainedMean-Shifts  这篇文章综述了ASM/CLM

CLM开源代码较少,但是有很好的从算法到工程实现的代码。

Constrained LocalModel (CLM) Implementation - Xiaoguang Yan

很多学者刚接触到人脸对齐时,不知道它有什么用处,下面就列举出人脸对齐的应用领域:
(1)人脸器官定位、器官跟踪。通过人脸对齐,我们能够定位到人脸的每个部件,提取相应的部件特征。


(2)表情识别。通过人脸对齐后,我们能够利用对齐后的人脸形状分析人脸的表情状态。

 

 

 


(3)人脸漫画/素描图像生成。通过人脸对齐后,我们能够进行人脸漫画和素描生成。
如:魔漫相机

 

(4)虚拟现实和增强现实。通过人脸对齐后,我们能够做出很多好玩的应用。如
2D应用:

 

3D应用:

 

(5)人脸老化、年轻化、年龄推断。特征融合/图像增强。通过人脸对齐后,我们能够有效提取人脸特征,并分析人脸年龄、人脸老化等。

 

 

(6)纹理过渡。如:长得很像某人的狗脸。
换脸

 

 

7)性别鉴别。通过人脸对齐,能够对人脸进行性别识别,男女之间的人脸形状有一定的差异性。

 

 

8)3D卡通。通过人脸对齐能够进行3D卡通模拟。

 

 

 

人脸对齐应用广泛,有着巨大的研究价值。学者们一定要好好研究哦!



为什么 web 开发人员需要迁移到. NET Core, 并使用 ASP.NET Core MVC 构建 web 和 webservice/API - 张善友 - 博客园

$
0
0

2018 .NET开发者调查报告: .NET Core 是怎么样的状态,这里我们看到了还有非常多的.net开发人员还在观望,本文给大家一个建议。这仅代表我的个人意见, 我有充分的理由推荐.net 程序员使用 . net core而不是 . net Framework。有些人可能不同意我的观点, 但是分享想法和讨论它是好的。.net 程序员或他们所在的团队总有各种理由说他们的系统还在使用旧系统, 这显然是企业开发人员的事情。所以, 我将列出一些关于谁应该迁移到使用. net core而不是. net 框架。以下是我的想法:

  1. 如果您是 旧式Windows 服务、web 应用程序或 web 服务的维护者, 则您需要继续使用. NET 框架。
  2. 如果您的应用程序将部署在旧的 Windows 服务器上,比如windows 2003/xp, 您需要继续使用. NET 框架。
  3. 如果您确信您的系统近期不会部署到云中, 那么您现在就可以继续使用. NET 框架。
  4. 如果您对使用. net 框架没有任何选择,比如来自公司的要求, 您需要继续使用. net 框架,这种情况对你的发展是不利的,我劝你学习.net core, 换家更有追求的公司。
初学者, 只学习. NET Core!

如果你是一个初学者开始学习 ASP.NET 或 ASP.NET MVC, 你可能并不知道什么是. net Framework和. net ore。不用担心!我建议您看下官方文档 https://docs.microsoft.com/zh-cn/aspnet/index, 您可以轻松地看到比较和差异。下面是我可以分享的一些. net framework和. net core的部分, 您可以点击每个链接以获取开发的工具。

 .NET Framework.NET Core
TechnologyFirst Release 2002 (Mature)First Release 2016 (Mature)
Latest Version4.7.2 ( Reference Source)2.0.6 ( Open Source Software)
SDK Version 2.1.3
SDKWindowsOnly (Version 7, 8, 10)Windows(Version 7, 8, 10),
Linux(redhat, Ubuntu >14.04, Fedora, Debian, CentOS 7, openSUSE 24, Oracle Linux 7, SLES 12),
Mac
ASP.NET Performance57,843 Request/Seconds (Plain Text)1,822,366 Request/Seconds (Plain Text)
Here is some real world news as reference: ASP.NET Core – 2300% More Requests Served Per Second.
Best IDE/EditorVisual Studio 2017 Community(latest, FREE)Visual Studio 2017 Community(latest, FREE),
Visual Studio Codefor Windows, Linux and Mac (FREE),
Visual Studio for Mac Community(FREE)
Web FrameworkWeb Form, ASP, MVC ( Weband Web API)MVC Core ( Web, Razor Page, Web API)
Entity Framework (ORM)Entity Framework 6.2 (latest)
(Microsoft SQL Server, Oracle, MySQL (Official), PostgreSL, SQLite, IBM Data Server (DB2))
Entity Framework Core 2.0.1 (latest)
(InMemory (for Testing), Microsoft SQL Server, SQLite, PostgreSQL (Npgsql), IBM Data Server (DB2), MySQL (Official), MySQL (Pomelo), Microsoft SQL Server Compact Edition, Devart (MySQL, Oracle, PostgreSQL, SQLite, DB2, and more),
Oracle (not yet available), MyCat, Firebird-Community)
FrontendPlain MVC, Angular (mostly using MVC)Plain MVC, Angular, React, and Redux

 

只需要5分钟入门使用. NET Core

如果您在移动笔记本上看这篇文章, 请下载并安装当前操作系统 (Windows、Linux、Mac) 的 . NET Core SDK。您不需要安装 Visual Studio 2017 就可以使用命令行开发. NET Core应用。你完成安装 SDK 后, 打开 ShellPowerShell(在 Windows 中)、 终端(在 Linux 或 Mac 中), 输入以下命令:

dotnet new console -o myApp
cd myApp
dotnet run

恭喜您, 你已经使用. NET Core 创建了第一个控制台应用程序。现在, 您可以通过将此应用程序发布到所需的任何平台来部署。在 Mac 机上,

dotnet publish --runtime osx-x64

或者安卓,

dotnet publish --runtime Android

下面是 runtime identifier catalog的完整列表。那么, 这个命令实际上做了什么:

dotnet new console -o myApp

当我们运行这个命令时, 它实际上是在文件夹中创建一个 控制台应用程序项目 myApp。如果您查看文件夹 myApp, 则应该看到以下文件

myApp.csproj
Program.cs

文件本身只是一个简单的Hello world。 Program.cs

using System;
 
namespace myApp
{
     class Program
     {
         static void Main(string[] args)
         {
             Console.WriteLine("Hello World!");
         }
     }
}

如果执行此命令,

dotnet new --list

它将从框中列出可用模板。这是你可能会看到的,

如果用mvc替换console时, 它将创建一个使用 ASP.NET core MVC的 web 应用程序项目。

迁移到 ASP.NET Core 意味着迁移到现代 Web 应用程序

我可以转移到 ASP.NET Core吗?简单的回答当然是 否, 您不能仅仅是使用 Visual Studio 2017 打开旧的 ASP.NET Web Form或 mvc 5就可以完成转换到 ASP.NET Core mvc。你可以参考以下老代码迁移策略:

  1. 如果你的的 web 应用程序使用 web form, 则不能直接将其转换或迁移到 ASP.NET Core。 因为 Web form和 MVC 有着完全不同的体系结构模型。MVC 使用模型、视图和控制器的分离。也没有 webform 控件组件 (如 web 窗体)。ASP.NETCore MVC 使用纯 HTML5 元素。当然您可以使用 TagHelpers来创建自定义 HTML 属性, 这些特性将转换为普通 HTML5。
  2. 如果你的 web 应用程序使用的是 ASP.NET MVC 5, 则你可以首先创建一个新的 ASP.NET Core MVC 项目, 复制粘贴某些代码到 ASP.NET Core。这将需要一些调整, 特别是在RazorPage。
  3. 如果你的 web 应用程序只是一个 web api , 则你可以首先创建一个新的 ASP.NET Core Web API项目,不是简单复制一些代码。这里需要做些调整, 因为 ASP.NET Core web api 使用的是 web api 2。
  4. 使用 HTML5! HTML5 仅用于现代 web 应用程序标准。使用来自 W3C(万维网 联合体) 标准的 HTML5。而不仅仅是来自 Microsoft EdgeChromeFirefox等的标准。因为每个浏览器都有自己的功能, 称为 平台标准, 而某些浏览器平台功能不成为 W3C 标准。如果只使用 chrome 功能标准, 则 web 可能只会被 chrome 绑定, 并且无法在其他浏览器上打开。并非所有浏览器都支持其他浏览器功能。
  5. 响应式布局,响应式布局,响应式布局! 重要的事情要说三遍。我们时常听到开发者说, " 不, 我们不需要响应式布局, 这只是桌面浏览器, 我们只是让它静态布局"。如果你的网站to c的, 你必须做响应式布局, 因为现在移动时代,更多的用户将使用他们的ios/android上面的移动浏览器查看。
  6. 不要在开发项目中直接使用 CSS。使用 SASS或者 LESS.。您可以使用 ASP.NET Core轻松完成此项任务。您甚至可以添加Gulp、Grunt或者webpack来编译 CSS。

如果您的代码遵循 S.O.L.I.D Principle原则进行正确的开发, 我相信, 迁移工作应该是很容易的, 而不是太多的调整。但是, 如果您的代码是意大利面条,则需要进行重构,.NET Core默认就使用依赖关系注入。这是ASP.NET常见最佳实践,当然是现在做更好, 而不是不做。好处也很多, 你可以学到一些新的东西, 您的新 web 应用程序将具有更好的性能, 更加现代化和可维护性。

对老板说:迁移到. NET Core

我知道你的老板会回答什么, 是的, 那 恐怖语句 " 兼容吗?"

我的建议是说: " 是的, 它是兼容的! 他们都是. NET。但我们需要一些调整, 一些需要小的编码更改,以符合编程的最佳实践"

我们需要迁移到.NET Core的精神是, 如果不是现在, 那么何时?现在马上就有发布.NET Core 2.1, 一切都改变了。技术发生了变化 ( 现代 Web 应用程序、移动、增强现实等)、基础结构已更改 ( 云、AI)、开发体系结构也已更改 ( 容器、无服务器) 等。

咱们这行业不尊重传统,只尊重创新. --- 微软CEO Satya Nadella

没那么容易

答案是肯定的。但是, 并不意味着不可能。它需要勇气和知识。 愿. NET 力量与您同在!

【实验手册】使用Visual Studio Code 开发.NET Core应用程序 - 张善友 - 博客园

$
0
0

.NET Core with Visual Studio Code

目录


概述

开源和跨平台开发是Microsoft 的当前和将来至关重要的策略。.NET Core已开源,同时开发了其他项来使用和支持新的跨平台策略。.NET Core 2.0 目前已经正式发布,是适用于针对 Web 和云构建跨平台应用程序的最新开源技术,可在 Linux、Mac OS X 和 Windows 上运行。

.NET Core使用各种命令行工具来生成基架、构建和运行应用程序,同时可以使用 Visual Studio Code 进行编辑。

Visual Studio Code 是微软为广大开发人员提供的免费开源的跨平台代码编辑器,和其它流行的代码编辑器,如:Sublime, Atom一样,它非常小,运行速度快,同时通过各种插件支持不同开发语言的编写。不同的地方在于,VSC的插件不仅仅提供静态的语言高亮,自动语法检测和完成功能外;还提供更加高级的编译器服务支持,这使得VSC可以在一定程度上替代IDE的功能,进行代码的编译,调试和发布操作。

本实验将介绍如何开发.NET Core跨平台应用程序,以及如何在 Linux、OS X 和 Windows 上的 Visual Studio Code ( code.visualstudio.com) 中编写代码。

先决条件

1. 为了完成这个实验,你需要一个Windows 10虚拟机

2. 你需要在虚拟机上手动安装.NET Core和Visual Studio Code,你可以按照练习1 的任务1来搭建实验环境

3. 你需要下载和安装Visual Studio Code的C#扩展,你可以按照练习1的任务2来完成

练习1: 安装和配置.NET Core以及Visual Studio Code 扩展

在本练习中,您将了解安装和配置 Visual Studio Code 和.NET Core扩展出于演示目的所需的安装和配置要点

任务1:安装Visual Studio Code和.NET Core

1. 下载Visual Studio Code ,从 https://code.visualstudio.com/下载最新版本并安装

clip_image002

2. 从 https://www.microsoft.com/net/core下载.NET Core 2.0进行安装

clip_image004

3. Node.JS和NPM,以及 bower, gulp 和 grunt 等前端工具, Node.js是一个javascript的运行引擎,提供服务端的javascript运行能力,同时也包含了npm这个包管理器,可以用来安装 bower, glup,grunt等前端工具。下载地址: http://nodejs.org

安装完成后,让通过以下命令安装前端工具

npm install bower gulp grunt-cli -g

clip_image006

任务2:安装插件

安装好Visual Studio Code 之后,需要安装下面插件

1. C# 扩展 http://www.omnisharp.net/

2. 安装vscode-nuget-package-manager

您可以在Visual Studio Code中界面安装这些扩展(选择菜单查看-扩展), 也可以使用 Ctrl + P, 然后输入 ext install vscode-nuget-package-manager或 ext install csharp。一旦您安装了 NuGet 项目管理器, 您还将使用它来安装 NuGet 包。

clip_image008

练习2:使用命令行界面构建. NET Core应用程序

.NET Core CLI 是开发 .NET Core 应用程序的一个新的跨平台工具链的基础。它是“基础”的原因时它是在其它的、高级别工具的主要层,如集成开发环境(IDEs),由编辑器和构建者组成。

默认它是跨平台的,并且对支持的每个平台有相同的表现范围。这意味着,当你学会如何使用工具,你可以从任何支持的平台上以同样的方式使用它。

本练习中现在我们假设你已经安装好了VS Code开发工具、.Net Core 2.0 SDK dotnet-sdk-2.0.0(注意自己的操作系统),并且已经为VS Code安装好了C#扩展。

一、 我们先在我们的电脑硬盘的新建一个文件夹。我把这个地方选在D:\WorkTest下,创建的文件夹名称为HelloWorld。注意,这一步不是在VS Code中完成的,VS Code中不能创建文件夹。

二、 在VS Code开发环境中,选择 文件->打开文件夹,然后选择我们刚刚创建文件夹HelloWorld打开

三、 选择 查看->集成终端 命令或直接摁下快捷键Ctrl+`,VS Code开发环境中会出现一个集成的终端。比如我接下来在集成终端中输入命令dotnet new sln -n HelloWorld,在我们的HelloWorld文件夹下会出现一个解决方案HelloWorld.sln

clip_image010

四、 接下来,我们再在集成终端中输入dotnet new mvc -n HelloWorld.Web,经过VS Code一阵的挣扎和折腾,我们会发现左边的文件列表中多了一个叫HelloWorld.Web的MVC项目。如下图:

clip_image012

五、 按下F5,选择.NET Core, 出来一个launch.json,如下图:

clip_image014

六、 在集成终端中输入命令dotnet build HelloWorld.Web命令,回车,完事后成成了一个东西在Debug下边。好了,我们把这段生成的东西放在launch.json的.NET Core Launch (web)配置项的program中,然后顺便改一下下边那个cwd。完事后看起来效果是下边这样子的:

clip_image016

七、 点击左边的那个小虫子,VS Code中的左侧会出现上图中的情况,选择.NET Core Launch (web),再次摁下F5,点击“配置任务运行程序”,自动生成了下边这个配置json文件:

clip_image018

八、 我们现在配置一下这个新生成的task.json吧。也就是在tasks配置项中添加如下内容,指定一下任务在build时的直接命令对象。${workspaceRoot}的意思就是你项目的根目录,别写成绝对路径,不然回头你项目发布后找不到

clip_image020

九、 再次按下F5, 一个.Net Core MVC网站出现在我们眼前

clip_image022

有可能会出现下面的错误:

clip_image024

这是由于默认启动的是 .NET Core Launch (console),点击调试(左边小虫子图标),选择.NET Core Launch (web)

clip_image025

十、 下面我们演示给项目添加一个Nuget包

利用我们前面安装的VS code 的Nuget 包扩展插件,我们选中HelloWorld.Web 项目,使用UI菜单查看- 命令面板:

clip_image027

选择NuGet Package Manager:Add Package, 回车,输入NodaTime , 选择版本后提示添加成功,打开HelloWorld.Web.csproj 文件可以看到NodaTime已经添加到工程中

clip_image029

打开 HomeController 文件,加入下述代码:

clip_image031

在Views\Home\Index.cshtml 加入下面代码

clip_image033

练习3:使用 Visual Studio Code和 Omnisharp 调试 c# 代码

上面的练习我们已经看到项目下有个.vscode 文件夹。里面有2个文件tasks.json 和launch.json.

clip_image034

tasks.json 用于dotnet 构建任务的配置数据,当你按下F5 启动调试时VS Code 可以生成项目

{

// See https://go.microsoft.com/fwlink/?LinkId=733558

// for the documentation about the tasks.json format

"version": "2.0.0",

"tasks": [

{

"taskName": "build",

"command": "dotnet build",

"type": "shell",

"group": "build",

"presentation": {

"reveal": "silent"

},

"problemMatcher": "$msCompile",

"options": {

"cwd": "${workspaceRoot}/HelloWorld.Web"

}

}

]

}

launch.json包含调试模式的配置数据,默认是从vs code启动或者附加进程。

launch.json中有很多属性可以设置, 通过智能提示查看有那些属性可以设置, 如果要查看属性的具体含义, 可以把鼠标悬停在属性上面, 会属性的使用说明.

任务1:从VS code启动调试器

对于控制台和Web项目是非常简单的,只需在代码中设置断点,导航到调试窗口(ctrl + shift + d)并点击调试按钮 - “.Net Core Launch”选项应该默认选择。您的应用程序现在应该停止在您的断点。

clip_image036

任务2:附加到进程/网站

使用VsCode将调试器附加到正在运行的进程也非常简单,设置断点,从调试菜单中选择“.Net Core Attach”选项,然后进行调试。任务栏应显示您可以选择附加调试器的正在运行的进程的列表 - 在本示例中,我们将附加到正在运行的dotnet网站进程。

clip_image038

练习4: 使用Visual Studio Code 开发ASP.NET Core 应用程序

本练习要使用Visual studio code完成一个包含多个项目的解决方案,包括类库和Web项目。结合Visual Studio Code和.NET Core CLI,创建项目结构如下:

piedpiper

└── src

├── piedpiper.domain

├── piedpiper.sln

├── piedpiper.tests

└── piedpiper.website

任务1:创建解决方案

首先,我们将创建我们的解决方案(.sln)文件,我一直都喜欢在顶级源文件夹中创建解决方案文件D:\WorkTest\piedpiper, 打开Visual Studio Code的集成终端,

PS D:\WorkTest\piedpiper> cd src

PS D:\WorkTest\piedpiper\src> dotnet new sln -n piedpiper

这将创建一个sln名为的新文件piedpiper.sln。

接下来,我们使用dotnet new <projecttype>命令中的output参数在特定文件夹中创建一个项目:

PS D:\WorkTest\piedpiper\src> dotnet new mvc -o piedpiper.website

已成功创建模板“ASP.NET Core Web App (Model-View-Controller)”。

这将在同一目录中的piedpiper.website文件夹中创建一个ASP.NET Core MVC应用程序。如果我们目前看到我们的文件夹结构,它看起来像这样:

接下来我们可以为我们的域名和测试项目做同样的事情:

PS D:\WorkTest\piedpiper\src> dotnet new classlib -o piedpiper.domain

PS D:\WorkTest\piedpiper\src> dotnet new xunit -o piedpiper.tests

任务2:将项目添加到我们的解决方案中

在这一点上,我们有一个没有引用项目的解决方案文件,我们可以通过调用list命令来验证这一点:

PS D:\WorkTest\piedpiper\src> dotnet sln list

未在解决方案中找到项目。

接下来我们将我们的项目添加到我们的解决方案文件,我们很容易在Visual Studio 2017中打开解决方案,然后手动添加对每个项目的引用。Visual Studio Code也可以通过.NET Core CLI完成。

现在开始使用以下命令添加每个项目,我们通过引用.csproj文件来执行此操作:

PS D:\WorkTest\piedpiper\src> dotnet sln add piedpiper.website/piedpiper.website.csproj

PS D:\WorkTest\piedpiper\src> dotnet sln add piedpiper.domain/piedpiper.domain.csproj

PS D:\WorkTest\piedpiper\src> dotnet sln add piedpiper.tests/piedpiper.tests.csproj

注意:如果您使用的是基于Linux / Unix的shell,您可以使用globbing模式在单个命令中执行此操作!

dotnet sln add **/*.csproj

现在,当我们调用list我们的解决方案文件时,我们应该得到以下输出:

S D:\WorkTest\piedpiper\src> dotnet sln list

项目引用

----

piedpiper.website\piedpiper.website.csproj

piedpiper.domain\piedpiper.domain.csproj

piedpiper.tests\piedpiper.tests.csproj

任务3:向项目添加项目引用

接下来,我们要开始向我们的项目添加项目引用,通过dotnet add reference命令将我们的域库链接到我们的网站和单元测试库:

PS D:\WorkTest\piedpiper\src> dotnet add piedpiper.tests reference piedpiper.domain/piedpiper.domain.csproj

已将引用“..\piedpiper.domain\piedpiper.domain.csproj”添加到项目。

现在,如果要查看测试项目的内容,我们将看到我们的domain 已被引用:

PS D:\WorkTest\piedpiper\src> cd .\piedpiper.tests\

PS D:\WorkTest\piedpiper\src\piedpiper.tests> cat .\piedpiper.tests.csproj

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>

<TargetFramework>netcoreapp2.0</TargetFramework>

<IsPackable>false</IsPackable>

</PropertyGroup>

<ItemGroup>

<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />

<PackageReference Include="xunit" Version="2.2.0" />

<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />

</ItemGroup>

<ItemGroup>

<ProjectReference Include="..\piedpiper.domain\piedpiper.domain.csproj" />

</ItemGroup>

</Project>

接下来,我们将为我们的网站项目做同样的事情,所以让我们去我们的网站文件夹并运行相同的命令:

dotnet add piedpiper.website reference piedpiper.domain/piedpiper.domain.csproj

如果我们返回到我们的根源文件夹并运行build命令,我们应该看到所有的构建成功:

PS D:\WorkTest\piedpiper\src> dotnet build

用于 .NET Core 的 Microsoft (R) 生成引擎版本 15.3.409.57025

版权所有(C) Microsoft Corporation。保留所有权利。

piedpiper.domain -> D:\WorkTest\piedpiper\src\piedpiper.domain\bin\Debug\netstandard2.0\piedpiper.domain.dll

piedpiper.tests -> D:\WorkTest\piedpiper\src\piedpiper.tests\bin\Debug\netcoreapp2.0\piedpiper.tests.dll

piedpiper.website -> D:\WorkTest\piedpiper\src\piedpiper.website\bin\Debug\netcoreapp2.0\piedpiper.website.dll

已成功生成。

0 个警告

0 个错误

任务4:将NuGet包添加到项目或更新它

假设我们要将NuGet包添加到我们的一个项目中,我们可以使用该add package命令来执行此操作。

首先导航到要添加NuGet软件包的项目:

PS D:\WorkTest\piedpiper\src> cd .\piedpiper.tests\

PS D:\WorkTest\piedpiper\src\piedpiper.tests> dotnet add package shouldly

Writing C:\Users\dell\AppData\Local\Temp\tmp68E4.tmp

info : Adding PackageReference for package 'shouldly' into project 'D:\WorkTest\piedpiper\src\piedpiper.tests\piedpiper.tests.csproj'.

log : Restoring packages for D:\WorkTest\piedpiper\src\piedpiper.tests\piedpiper.tests.csproj...

info : GET https://api.nuget.org/v3-flatcontainer/shouldly/index.json

info : OK https://api.nuget.org/v3-flatcontainer/shouldly/index.json 512ms

info : GET https://api.nuget.org/v3-flatcontainer/shouldly/2.8.3/shouldly.2.8.3.nupkg

info : OK https://api.nuget.org/v3-flatcontainer/shouldly/2.8.3/shouldly.2.8.3.nupkg 84ms

log : Installing Shouldly 2.8.3.

info : Package 'shouldly' is compatible with all the specified frameworks in project 'D:\WorkTest\piedpiper\src\piedpiper.tests\piedpiper.tests.csproj'.

info : PackageReference for package 'shouldly' version '2.8.3' added to file 'D:\WorkTest\piedpiper\src\piedpiper.tests\piedpiper.tests.csproj'.

或者,我们可以使用版本参数指定要安装的版本:

dotnet add package shouldly -v 2.8.3

更新NuGet包

将NuGet软件包更新到最新版本也是一样简单,只需使用相同的命令而不使用版本参数:

dotnet add package shouldly

你也可以下载word 文档 http://url.cn/5e3NT3G


异地双活的四个误区 - 旁观者 - 博客园

$
0
0

郑昀(老兵笔记) 20190305

阿里云华北二机房2019年3月3日凌晨服务中断长达三小时,我在微博上喊出了:工程师赶紧起床,切多活流量啊。


那么切多活有什么常见误区呢?

A,灾备(主备)还是双活?
多年前,大家往往做成了灾备机房,一主一备。结果是,真正灾难发生的时候,最高领导人下不了决心切机房,因为无法预料切换后果(灾难总是不期而遇,切过去就可能切不回来了)。
所以一定是多个数据中心同时运行着同样的应用,拥有同样的数据,任何一个客户的交易可以在分钟级全部路由到另一个中心并对外提供服务,不至于说灾难来临时才发现集群无法工作。

B,双活测试模拟正常流量切换就够了吗?
不是模拟在正常情况下的多活切换,那怎么测怎么有。
而是模拟灾难发生(突然发生)的时候,另外一个机房物理消失了,你该如何切换。
我们过去犯的两个错误是:
-用代码逻辑限制双活机房之间的数据库同步不能延时超过N分钟,超过了就阻止切换;
-限制双活机房的 otter 服务访问超时时间不得超过N分钟,超过了就阻止切换。
问题就在于,真正灾难发生的时候,机房已物理不可访问了,这时候就是要立刻地、全部地切换流量,人下达的命令就是最终裁决。拼着损失一分钟的交易和脏数据,也要把交易切到另一个机房。

C,所有业务都双活吗?
基于互联网公司常用的基本可用性保障原则,只是保障核心业务双活。
怎么定义核心业务?即不能容忍中断的服务。
用户注册,商户进件,这些都属于能容忍临时性中断的服务。
非核心业务应用都被标记为非多活业务,非多活数据库与多活数据库要严格区分开来。

D,切机房的时候直接切吗?
双活意味着两个机房都不需要维护一个能承载所有流量的集群,否则太费钱。
所以模拟切机房流量的时候,一定要测试与核心业务有关的所有应用自动扩容,扩容之后再切换流量。测试扩容的效率,分钟级扩容完毕。
所以你的应用最好都是部署在Docker容器集群上的,这样才能做到扩容分钟级。
而且大家一般是混合云部署,所以在不同的云平台上,你的应用部署底层基础最好都一模一样,方便你扩容和切换。

-EOF-
欢迎订阅老兵笔记:

 

Kubernetes集群搭建之企业级环境中基于Harbor搭建自己的私有仓库 - 鬼谷君 - 博客园

$
0
0

image

搭建背景


企业环境中使用Docker环境,一般出于安全考虑,业务使用的镜像一般不会从第三方公共仓库下载。那么就要引出今天的主题

企业级环境中基于Harbor搭建自己的安全认证仓库

介绍


名称:Harbor

官网:https://github.com/vmware/harbor

简介:Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器,通过添加一些企业必需的功能特性,例如安全、标识和管理等,扩展了开源Docker Distribution。作为一个企业级私有Registry服务器,Harbor提供了更好的性能和安全。提升用户使用Registry构建和运行环境传输镜像的效率。Harbor支持安装在多个Registry节点的镜像资源复制,镜像全部保存在私有Registry中, 确保数据和知识产权在公司内部网络中管控。另外,Harbor也提供了高级的安全特性,诸如用户管理,访问控制和活动审计等。

部署Harbor


Harbor是基于Docker-Compose进行编排的,需要配合Docker和Docker-compose使用。Docker的安装可以看我的另一篇 文章

下载Docker-Compose最新稳定版


[root@harbor-01 hub]# pwd/opt/hub[root@harbor-01 hub]# curl -L https://github.com/docker/compose/releases/download/1.23.2/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

ps: 可能会报错Peer reports incompatible or unsupported protocol version.,升级下curl就行

添加可执行权限


[root@harbor-01 hub]# chmod +x /usr/local/bin/docker-compose

验证版本


[root@harbor-01 hub]# docker-compose -vdocker-compose version 1.23.2, build 1110ad01

解压安装包



[root@harbor-01 hub]# ls-rw-r--r-- 1 root root 541535889 Mar 4 18:52 harbor-offline-installer-v1.7.3.tgz[root@harbor-01 hub]# tar xf harbor-offline-installer-v1.7.3.tgz

修改配置


主要修改hostname字段配置

[root@harbor-01 hub]# cd harbor/[root@harbor-01 hub]# vim harbor.cfghostname = hub.test.tech # 本机外网IP或域名,该地址供用户通过UI进行访问,不要使用127.0.0.1ui_url_protocol = http # 用户访问私仓时使用的协议,默认时httpdb_password = root123    # 指定mysql数据库管理员密码harbor_admin_password:Harbor12345 # harbor的管理员账户密码

通过官方一键脚本安装


[root@harbor-01 hub]#./install.sh......[Step 4]: starting Harbor ...Creating network "harbor_harbor" with the default driverCreating harbor-log ... doneCreating harbor-db ... doneCreating registryctl ... doneCreating registry ... doneCreating harbor-adminserver ... doneCreating redis ... doneCreating harbor-core ... doneCreating harbor-portal ... doneCreating harbor-jobservice ... doneCreating nginx ... done✔ ----Harbor has been installed and started successfully.----Now you should be able to visit the admin portal at http://hub.test.tech. For more details, please visit https://github.com/goharbor/harbor .

测试登陆


[root@harbor-01 harbor]# docker login hub.test.techUsername: adminPassword: WARNING! Your password will be stored unencrypted in /root/.docker/config.json.Configure a credential helper to remove this warning. Seehttps://docs.docker.com/engine/reference/commandline/login/#credentials-storeLogin Succeeded

在windows上用域名访问需要绑定下hosts即可

image

默认账号密码 admin  Harbor12345

使用


我这里创建个mytest的公开项目做测试(不使用默认的library)。公开项目pull不需要登录,push需要登录

image

接下来我们试下推个镜像到Harbor的mytest项目中,这里我以公共镜像goharbor/nginx-photon:v1.7.3镜像为例 ,需要注意的是要往私有仓库推镜像就得打个tag才行 指明要推往哪个仓库并标注标签

注意:我这里使用的这个域名是自定义的,那么需要在需要上传下载镜像的机器上添加hosts绑定,因为我这没开启https所有也要修改docker配置

[root@harbor-01 ~]# cat /etc/docker/daemon.json {"insecure-registries": ["hub.test.tech"]}

重启docker即可

[root@harbor-01 harbor]# docker images|grep nginx-photongoharbor/nginx-photon v1.7.3 9d8222585538 3 weeks ago 35.6MB[root@hub harbor]# docker tag goharbor/nginx-photon:v1.7.3 hub.test.tech/mytest/nginx-photon:v1[root@harbor-01 harbor]# docker push hub.test.tech/mytest/nginx-photon:v1The push refers to repository [hub.test.tech/mytest/nginx-photon]f08bfdb20f6f: Pushed 8e45c790c209: Pushed v1: digest: sha256:03d473217d79c40c3b4e0d6015098f8d16364707980e12b5e7330ac76938d16a size: 739

可以看到push成功,我们去页面上看看

image

pull演示


点击镜像详情可以看到具体标签版本,鼠标放在"pull命令"图标上可以获取命令

image

[root@harbor-01 harbor]# docker pull hub.test.tech/mytest/nginx-photon:v1v1: Pulling from mytest/nginx-photonDigest: sha256:03d473217d79c40c3b4e0d6015098f8d16364707980e12b5e7330ac76938d16aStatus: Downloaded newer image for hub.test.tech/mytest/nginx-photon:v1

本篇介绍了Harbor的基本部署和使用,更多高级使用方法后续会分享。下一章介绍部署Etcd集群,敬请期待,谢谢!

往期文章一览

1、Kubernetes集群搭建之系统初始化配置篇

END

如果你觉得文章还不错,请大家点 『好看』分享下。你的肯定是我最大的鼓励和支持。


移动跨平台技术方案总结 - xiangzhihong8的专栏 - CSDN博客

$
0
0

“得移动端者得天下”,移动端取代PC端,成为了互联网行业最大的流量分发入口,因此不少公司制定了“移动优先”的发展策略。

为了帮助读者更好地学习WEEX,本节将对React Native、Weex和Flutter等主流的跨平台方案进行简单的介绍和对比。

React Native

React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的React框架在原生移动应用平台的衍生产物,目前主要支持iOS和安卓两大平台。

RN使用Javascript语言来开发移动应用,但UI渲染、网络请求等均由原生端实现。具体来说,开发者编写的Javascript代码,通过中间层转化为原生控件后再执行,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域,并可以在不牺牲用户体验的前提下提高开发效率。

作为一个跨平台技术框架,RN从上到下可以分为Javascript层、C++层和Native层。其中,C++层主要用于实现动态连结库(.so),作为中间适配层桥接,实现js端与原生端的双向通信交互,如下图所示是RN在Android平台上的通信原理图。
在这里插入图片描述

在RN的三层架构中,最核心的就是中间的C++层,C++层最核心的功能就是封装JavaScriptCore,用于执行对js的解析。同时,原生端提供的各种Native Module(如网络请求,ViewGroup控件模块)和JS端提供的各种JS Module(如JS EventEmiter模块)都会在C++实现的so文件中保存起来,最终通过C++层中的保存的映射实现两端的交互。
在这里插入图片描述

在RN开发过程中,大多数情况下开发人员并不需要需要了解RN框架的具体细节,只需要专注JS端的逻辑代码实现即可。但是需要注意的是,由于js代码是运行在独立的JS线程中,所以在js中不能处理耗时的操作,如fetch、图片加载和数据持久化等操作。

最终,JS代码会被打包成一个bundle文件并自动添加到应用程序的资源目录下,而应用程序最终加载的也是打包后的bundle文件。RN的打包脚本位于“/node_modules/react-native/local-cli”目录下,打包后通过metro模块压缩成bundle文件,而bundle文件只包含打包js的代码,并不包含图片、多媒体等静态资源,而打包后的静态资源会是被拷贝到对应的平台资源文件夹中。

总的来说,RN使用Javascript来编写应用程序,然后调用原生组件执行页面渲染操作,在提高了开发效率的同时又保留了Native的用户体验。并且,伴随着Facebook重构RN工作的完成,RN也将变得更快、更轻量、性能更好。

Weex

作为一套前端跨平台技术框架,Weex建立了一套源码转换以及Native与Js通信的机制。Weex表面上是一个客户端框架,但实际上它串联起了从本地开发、云端部署到分发的整个链路。
具体来说,在开发阶段编写一个.we文件,然后使用Weex提供的weex-toolkit转换工具将.we文件转换为JS bundle,并将生成的JS bundle上传部署到云端,最后通过网络请求或预下发的方式加载至用户的移动应用客户端。当集成了Weex SDK的客户端接收到JS bundle文件后,调用本地的JavaScript引擎执行环境执行相应的JS bundle,并将执行过程中产生的各种命令发送到native端进行界面渲染、数据存储、网络通信以及用户交互响应。
在这里插入图片描述
由上图可知,Weex框架中最核心的部分就是JavaScript Runtime。具体来说,当需要执行渲染操作时,在iOS环境下选择基于JavaScriptCore内核的iOS系统提供的JSContext,在Android环境下使用基于JavaScriptCore内核的JavaScript引擎。

当JS bundle从服务器下载完成之后,Weex的Android、iOS和H5会运行一个JavaScript引擎来执行JS bundle,同时向各终端的渲染层发送渲染指令,并调度客户端的渲染引擎实现视图渲染、事件绑定和处理用户交互等操作。
由于Android、iOS和H5等终端最终使用的是native渲染引擎,也就是说使用同一套代码在不同终端上展示的样式是相同的,并且Weex使用native引擎渲染的是native组件,所以在性能上比传统的WebView方案要好很多。

当然,尽管Weex已经提供了开发者所需要的最常用的组件和模块,但面对丰富多样的移动应用研发需求,这些常用基础组件还是远远不能满足开发的需要,因此Weex提供了灵活自由的扩展能力,开发者可以根据自身的情况定制属于自己客户端的组件和模块,从而丰富Weex生态。

Flutter

Flutter是Google开源的移动跨平台框架,其历史最早可以追溯到2015年的Sky项目,该项目可以同时运行在Android、iOS和fuchsia等包含Dart虚拟机的平台上,并且性能无限接近原生。相较于RN和Weex使用Javascript作为编程语言与使用平台自身引擎渲染界面不同,Flutter直接选择2D绘图引擎库skia来渲染界面。

在这里插入图片描述
如上图所示,Flutter框架主要由Framework和Engine层组成,而我们基于Framework开发App最终会运行在Engine上。其中,Engine是Flutter提供的独立虚拟机,正是由于它的存在Flutter程序才能运行在不同的平台上,实现跨平台运行的能力。
与RN和Weex使用原生控件渲染界面不同,Flutter并不需要使用原生控件来渲染界面,而是使用Engine来绘制Widget(Flutter显示单元),并且Dart代码会通过AOT编译为平台的原生代码,实现与平台的直接通信,不需要JS引擎的桥接,也不需要原生平台的Dalvik虚拟机,如图1-5所示。
同时,Flutter的Widget采用现代响应式框架来构建,而Widget是不可变的,仅支持一帧,并且每一帧上的内容不能直接更新,需要通过Widget的状态来间接更新。在Flutter中,无状态和有状态Widget的核心特性是相同的,视图的每一帧Flutter都会重新构建,通过State对象Flutter就可以跨帧存储状态数据并恢复它。

在这里插入图片描述
总的来说,Flutter是目前跨平台开发中最好的方案,它以一套代码即可生成Android和iOS平台两种应用,很大程度上减少了App开发和维护的成本,同时Dart语言强大的性能表现和丰富的特性,也使得跨平台开发变得更加便利。而不足的是,Flutter还处于Alpha阶段,许多功能还不是特别完善,而全新的Dart语言也带来了学习上的成本,如果想要完全替代Android和iOS开发还有比较长的路要走。

PWA

PWA,全称Progressive Web App,是Google在2015年提出渐进式的网页技术。PWA结合了一系列的现代Web技术,并使用多种技术来增强Web App的功能,最终可以让网页应用呈现和原生应用相似的体验。

相比于传统的网页技术,渐进式Web技术是可以横跨Web技术及Native APP开发的技术解决方案,具有可靠、快速且可参与等诸多特点。

具体来说,当用户从手机主屏幕启动时,不用考虑网络的状态就可以立刻加载出PWA。并且,相比传统的网页加载速度,PWA的加载速度是非常快的,因为PWA使用了Service Worker 等先进技术。除此之外,PWA还可以被添加在用户的主屏幕上,不用从应用商店进行下载即可通过网络应用程序Manifest file提供类似于APP的使用体验。

作为一种全新Web技术方案,PWA的正常工作需要一些重要的技术组件,它们协同工作并为传统的Web应用程序注入活力,如图1-8所示。

在这里插入图片描述
其中,Service Worker表示离线缓存文件,其本质是Web应用程序与浏览器之间的代理服务器,可以在网络可用时作为浏览器和网络间的代理,也可以在离线或者网络极差的环境下使用离线的缓冲文件。

Manifest则是W3C一个技术规范,它定义了基于JSON的清单,为开发人员提供一个放置与Web应用程序关联的元数据的集中地点。Manifest是PWA 开发中的重要一环,它为开发人员控制应用程序提供了可能。

目前,渐进式Web应用还处于起步阶段,使用的厂商也是诸如Twitter、淘宝、微博等大平台。不过,PWA作为Google主推的一项技术标准,Edge、Safari和FireFox等主流浏览器也都开始支持渐进式Web应用。因此,可以预见的是,PWA必将成为继移动之后的又一革命性技术方案。

对比

在当前诸多的跨平台方案中,RN、Weex和Flutter无疑是最优秀的。而从不同的细节来看,三大跨平台框架又有各自的优点和缺点,可以通过表1-1来查看。

对比类型React NativeWeexFlutter
支持平台Android/IOSAndroid/IOS/WebAndroid/IOS
实现技术JavaScriptJavaScript原生编码/渲染
引擎JS V8JSCoreFlutter Engine
编程语言ReactVueDart
bundle包大小单一、较大较小、多页面不需要
框架程度较重较轻
社区活跃、FB维护不活跃活跃

如上表所示,RN、Weex采用的技术方案大体相同,它们都使用JavaScript作为编程语言,然后通过中间层转换为原生的组件后再利用Native渲染引擎执行渲染操作。而Flutter直接使用skia来渲染视图,而Flutter Widget则使用现代响应式框架来构建,和平台没有直接的关系。就目前跨平台技术来看,JavaScript在跨平台开发中可谓占据半壁江山,大有“一统天下”的趋势。
从性能方面来说,Flutter的性能理论上是最好的,RN和Weex次之,并且都好于传统的WebView方案。但从目前的实际应用来看却并没有太大的差距,特别是和0.5.0版本以上的RN对比性能体验上差异并不明显。
而从社群和社区的活跃来看,RN和Flutter无疑是最活跃的,RN经过4年多的发展已经成长为跨平台开发的实际领导者,并拥有各类丰富的第三方库和开发群体。Flutter作为最近才火起来的跨平台技术方案,不过目前还处在beta阶段,商用的实例也很少,不过应该看到google的号召力一直是很强,未来究竟如何发展让我们拭目以待。

示例

eros-yanxuan

简介

eros-yanxuan是基于 eros开发的Weex项目,部分页面参考了项目 网易严选 weex 版本,欢迎star或fork。

运行

确保你本地已经集成了 eros 开发所需的环境

clone 项目到本地:

$ git clone https://github.com/xiangzhihong/eros-yanxuan.git

进入目录,下载前端所需的依赖:

$ cd eros-yanxuan
$ npm install

iOS SDK

打开platforms目录下的WeexEros项目,在WeexEros中使用pod添加依赖。

$ cd platforms/ios/WeexEros
$ pod update                // 下载 iOS 依赖
$ open WeexEros.xcworkspace // 自动打开项目

选中模拟器,点击绿色箭头运行 app 即可。

Android

对于Android工程来说,使用Android Studio打开platforms目录下的WeexFrameworkWrapper的Android工程,然后使用install.sh安装Android工程的需要依赖包nexus和wxframework。

具体可以参考 自行导入项目,便可运行起来。

运行

  • 项目根目录下运行 eros dev
  • 关闭调试,拦截器,打开热更新
  • 重新 build app

效果

在这里插入图片描述

Question

运行过程中出现问题在以下地址解决方法,如果没有找到,可以参考 eros快速入门新建一个Weex工程,然后将src和配置文件的代码拷贝过去。 如果还有问题,请加群:515980159

搭建微服务器:express+https+api代理 - 馒头加梨子 - 博客园

$
0
0

概述

最近打算玩一下 service worker,但是service worker只能在https下跑,所以查资料自己用纯express搭建了一个微服务器,把过程记录下来,供以后开发时参考,相信对其他人也有用。

参考资料: express官方文档

http服务器

首先我们用express搭建一个 http服务器,很简单,看看官方文档就可以搭建出来了。代码如下:

// server.js
const express = require('express');
const http = require('http');

const app = express();
const PORT = 7088; // 写个合理的值就好
const httpServer = http.createServer(app);

app.get('/', function (req, res) {
  res.send('hello world');
});

httpServer.listen(PORT, function () {
  console.log('HTTPS Server is running on: http://localhost:%s', PORT);
});

加入到项目中

我们的 理想状况是,在项目目录下建立一个server文件夹,然后在server文件夹里面启动服务器,加载项目目录下的dist文件夹。

所以我们加入代码解析静态资源:

// server.js
const express = require('express');
const http = require('http');

const app = express();
const PORT = 7088; // 写个合理的值就好
const httpServer = http.createServer(app);

app.use('/', express.static('../dist'));

httpServer.listen(PORT, function () {
  console.log('HTTPS Server is running on: http://localhost:%s', PORT);
});

加入https

我们想把http变成https,首先我们要 生成本地证书

brew install mkcert
mkcert localhost 127.0.0.1 ::1

上面的代码意思是说,先安装mkcert,然后用mkcert给localhost,127.0.0.1和::1这三个域名生成证书。

然后我们可以在文件夹下面看到2个文件:

秘钥:example.com+3-key.pem
公钥:example.com+3.pem

我们在钥匙串里面把公钥添加信任。方法可参考: 在Vue里用Service Worker来搞个中间层(React同理)

添加完之后我们把秘钥和公钥放在certificate文件夹,然后添加到credentials.js文件中,我们通过这个文件引入秘钥和公钥:

// credentials.js
const path = require('path');
const fs = require('fs');

// 引入秘钥
const privateKey = fs.readFileSync(path.resolve(__dirname, './certificate/example.com+3-key.pem'), 'utf8');
// 引入公钥
const certificate = fs.readFileSync(path.resolve(__dirname, './certificate/example.com+3.pem'), 'utf8');

module.exports = {
  key: privateKey,
  cert: certificate
};

最后我们把 http变成https,并且引入秘钥和公钥:

// server.js
const express = require('express');
const https = require('https');
const credentials = require('./credentials');

const app = express();
const SSLPORT = 7081; // 写个合理的值就好
const httpsServer = https.createServer(credentials, app);

app.use('/', express.static('../dist'));

httpsServer.listen(SSLPORT, function () {
  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
});

设置api代理

在项目中,我们经常遇到跨域问题,在开发时我们是通过devServer的 proxyTable解决的,而proxyTable在打包后是无效的。所以我们需要在服务器上面 代理api请求。代码如下:

// proxy.js
const proxy = require('http-proxy-middleware');

const authApi = 'your-authApi-address';
const commonApi = 'your-commonApi-address';

module.exports = app => {
  app.use('/api/auth', proxy({
    target: authApi,
    changeOrigin: true,
    pathRewrite: {'/api/auth': '/auth'
    },
    secure: false,
  }));

  app.use('/api/common', proxy({
    target: commonApi,
    changeOrigin: true,
    pathRewrite: {'/api/common': '/api'
    },
    secure: false,
  }));
};

写法和devServer里面是一样的,因为devServer底层也是通过express实现的。

然后我们在server.js里面引入上面写的代理:

// server.js
const express = require('express');
const https = require('https');
const setProxy = require('./proxy');
const credentials = require('./credentials');

const app = express();
const SSLPORT = 7081; // 写个合理的值就好
const httpsServer = https.createServer(credentials, app);

app.use('/', express.static('../dist'));

setProxy(app);

httpsServer.listen(SSLPORT, function () {
  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
});

最后

最后我们把server.js,credentials.js和proxy.js放在一起就好了啦!

用法:只需要把整个文件夹放到项目目录,在里面运行下面的指令就好了:

yarn i
node server.js

详细代码可以参考 我的github

深度学习中的batch_size,iterations,epochs等概念的理解 - 控球强迫症 - 博客园

$
0
0

在自己完成的几个有关深度学习的Demo中,几乎都出现了batch_size,iterations,epochs这些字眼,刚开始我也没在意,觉得Demo能运行就OK了,但随着学习的深入,我就觉得不弄懂这几个基本的概念,对整个深度学习框架理解的自然就不够透彻,所以今天让我们一起了解一下这三个概念。


1.batch_size

深度学习的优化算法,用大白话来说其实主要就是梯度下降算法,而每次的参数权重更新主要有两种方法。

(1)遍历全部数据集算一次损失函数,然后算函数对各个参数的梯度,更新梯度

这种方法每更新一次参数都要把数据集里的所有样本都看一遍,计算量开销大,计算速度慢,不支持在线学习,这称为Batch gradient descent,批梯度下降。

(2)stochastic gradient descent

每看一个数据就算一下损失函数,然后求梯度更新参数,这个称为随机梯度下降。这个方法速度比较快,但是收敛性能不太好,可能在最优点附近晃来晃去,hit不到最优点。两次参数的更新也有可能互相抵消掉,造成目标函数震荡的比较剧烈。

为了克服两种方法的缺点,现在一般采用的是一种折中手段,mini-batch gradient decent,小批的梯度下降,这种方法把数据分为若干个批,按批来更新参数,这样,一个批中的一组数据共同决定了本次梯度的方向,下降起来就不容易跑偏,减少了随机性。另一方面因为批的样本数与整个数据

集相比小了很多,计算量也不是很大。

基本上现在的梯度下降都是基于mini-batch的,所以深度学习框架的函数中经常会出现batch_size,就是指这个意思。

2.iterations

iterations(迭代):每一次迭代都是一次权重更新,每一次权重更新需要batch_size个数据进行Forward运算得到损失函数,再BP算法(反向传播算法)更新参数。1个iteration等于使用batchsize个样本训练一次。

3.epochs

epochs被定义为向前和向后传播中所有批次的单次训练迭代。这意味着1个周期是整个输入数据的单次向前和向后传递。简单说,epochs指的就是训练过程中数据将被“轮”多少次,就这样。

接下来让我们看个例子:

假设训练集有1000个样本,batchsize=10,那么: 

训练完整个样本集需要: 100次iteration,1次epoch。

具体的计算公式为: one epoch = numbers of iterations = N = 训练样本的数量/batch_size

 

异步编程 In .NET - 腾飞(Jesse) - 博客园

$
0
0

概述

  在之前写的一篇关于 async和await的前世今生的文章之后,大家似乎在async和await提高网站处理能力方面还有一些疑问,博客园本身也做了不少的尝试。今天我们再来回答一下这个问题,同时我们会做一个async和await在WinForm中的尝试,并且对比在4.5之前的异步编程模式APM/EAP和async/await的区别,最后我们还会探讨在不同线程之间交互的问题。

  IIS存在着处理能力的问题,但是WinForm却是UI响应的问题,并且WinForm的UI线程至始至终都是同一个,所以两者之间有一定的区别。有人会问,现在还有人写WinForm吗?好吧,它确是一个比较老的东西呢,不如WPF炫,技术也不如WPF先进,但是从架构层面来讲,不管是Web,还是WinForm,又或是WPF,Mobile,这些都只是表现层,不是么?现在的大型系统一般桌面客户端,Web端,手机,平板端都会涉及,这也是为什么会有应用层,服务层的存在。我们在这谈论的ASP.NET MVC,WinForm,WFP,Android/IOS/WP 都是表现层,在表现层我们应该只处理与“表现”相关的逻辑,任何与业务相关的逻辑应该都是放在下层处理的。关于架构的问题,我们后面再慢慢深入,另外别说我没有提示您,我们今天还会看到.NET中另一个已经老去的技术Web Service。

  还得提示您,文章内容有点长,涉及的知识点比较多,所以,我推荐:”先顶后看“ ,先顶后看是21世纪看长篇的首选之道,是良好沟通的开端,想知道是什么会让你与众不同吗?想知道为什么上海今天会下这么大的雨么?请记住先顶后看,你顶的不是我的文章,而是我们冒着大雨还要去上班的可贵精神!先顶后看,你值得拥有!

目录

async/await如何提升IIS处理能力

  首先响应能力并不完全是说我们程序性能的问题,有时候可能你的程序没有任何问题,而且精心经过优化,可是响应能力还是没有上去,网站性能分析是一个复杂的活,有时候只能靠经验和不断的尝试才能达到比较好的效果。当然我们今天讨论的主要是IIS的处理能力,或者也可能说是IIS的性能,但绝非代码本身的性能。即使async/await能够提高IIS的处理能力,但是对于用户来说整个页面从发起请求到页面渲染完成的这些时间,是不会因为我们加了async/await之后产生多大变化的。

  另外异步的ASP.NET并非只有async/await才可以做的,ASP.NET在Web Form时代就已经有异步Page了,包括ASP.NET MVC不是也有异步的Controller么?async/await 很新,很酷,但是它也只是在原有一技术基础上做了一些改进,让程序员们写起异步代码来更容易了。大家常说微软喜欢新瓶装旧酒,至少我们要看到这个新瓶给我们带来了什么,不管是任何产品,都不可能一开始就很完美,所以不断的迭代更新,也可以说是一种正确做事的方式。

ASP.NET并行处理的步骤

    ASP.NET是如何在IIS中工作的一文已经很详细的介绍了一个请求是如何从客户端到服务器的HTTP.SYS最后进入CLR进行处理的(强烈建议不了解这一块的同学先看这篇文章,有助于你理解本小节),但是所有的步骤都是基于一个线程的假设下进行的。IIS本身就是一个多线程的工作环境,如果我们从多线程的视角来看会发生什么变化呢?我们首先来看一下下面这张图。注意:我们下面的步骤是建立在IIS7.0以后的集成模式基础之上的。(注:下面这张图在dudu的提醒之后,重新做了一些搜索工作,做了一些改动,w3dt这一步来自于博客园团队对问题的不断探索,详情可以点 这里

  我们再来梳理一下上面的步骤:

  1. 所有的请求最开始是由HTTP.SYS接收的,HTTP.SYS内部有一个队列维护着这些请求,这个队列的request的数量大于一定数量(默认是1000)的时候,HTTP.SYS就会直接返回503状态(服务器忙),这是我们的第一个阀门。性能计数指标:“ Http Service Request Queues\CurrentQueueSize
  2. 由w3dt负责把请求从HTTP.SYS 的队列中放到一个对应端口的队列中,据非官方资料显示该队列长度为能为20(该队列是非公开的,没有文档,所以也没有性能计数器)。
  3. IIS 的IO线程从上一步的队列中获取请求,如果是需要ASP.NET处理的,就会转交给CLR 线程池的Worker 线程,IIS的IO线程继续返回重复做该步骤。CLR 线程池的Worker线程数量是第二个阀门。
  4. 当CLR中正在被处理的请求数据大于一定值(最大并行处理请求数量,.NET4以后默认是5000)的时候,从IO线程过来的请求就不会直接交给Worker线程,而是放到一个进程池级别的一个队列了,等到这个数量小于临界值的时候,才会把它再次交给Worker线程去处理。这是我们的第三个阀门。
  5. 上一步中说到的那个进程池级别的队列有一个长度的限制,可以通过web.config里面的processModel/requestQueueLimit来设置。这可以说也是一个阀门。当正在处理的数量大于所允许的最大并行处理请求数量的时候,我们就会得到503了。可以通过性能计数指标:“ ASP.NET v4.0.30319\Requests Queued” 来查看该队列的长度。

  哪些因素会控制我们的响应能力

  从上面我们提到了几大阀门中,我们可以得出下面的几个数字控制或者说影响着我们的响应能力。

  1. HTTP.SYS队列的长度
  2. CLR线程池最大Worker线程数量
  3. 最大并行处理请求数量
  4. 进程池级别队列所允许的长度

HTTP.SYS队列的长度

  这个我觉得不需要额外解释,默认值是1000。这个值取决于我们我们后面IIS IO线程和Worker线程的处理速度,如果它们两个都处理不了,这个数字再大也没有用。因为最后他们会被存储到进程池级别的队列中,所以只会造成内存的浪费。

最大Worker线程数量

  这个值是可以在web.config中进行配置的。

  maxWorkerThreads: CLR中真实处理请求的最大Worker线程数量
  minWorkerThreads:CLR中真实处理请求的最小Worker线程数量

  minWorkerThreads的默认值是1,合理的加大他们可以避免不必要的线程创建和销毁工作。

最大并行处理请求数量

  进程池级别的队列给我们的CLR一定的缓冲,这里面要注意的是,这个队列还没有进入到CLR,所以它不会占用我们托管环境的任何资源,也就是把请求卡在了CLR的外面。我们需要在aspnet.config级别进行配置,我们可以在.net fraemwork的安装目录下找到它。一般是 C:\Windows\Microsoft.NET\Framework\v4.0.30319 如果你安装的是4.0的话。

  maxConcurrentRequestPerCPU: 每个CPU所允许的最大并行处理请求数量,当CLR中worker线程正在处理的请求之和大于这个数时,从IO线程过来的请求就会被放到我们进程池级别的队列中。
  maxConcurrentThreadsPerCPU: 设置为0即禁用。
  requestQueue: 进程池级别队列所允许的长度  

async和await 做了什么?

  我们终于要切入正题了,拿ASP.NET MVC举例,如果不采用async的Action,那么毫无疑问,它是在一个Woker线程中执行的。当我们访问一些web service,或者读文件的时候,这个Worker线程就会被阻塞。假设我们这个Action执行时间一共是100ms,其它访问web service花了80ms,理想情况下一个Worker线程一秒可以响应10个请求,假设我们的maxWorkerThreads是10,那我们一秒内总是可响应请求就是100。如果说我们想把这个可响应请求数升到200怎么做呢?

  有人会说,这还不简单,把maxWorkerThreads调20不就行了么? 其实我们做也没有什么 问题,确实是可以的,而且也确实能起到作用。那我们为什么还要大费周章的搞什么 async/await呢?搞得脑子都晕了?async/await给我们解决了什么问题?它可以在我们访问web service的时候把当前的worker线程放走,将它放回线程池,这样它就可以去处理其它的请求了。等到web service给我们返回结果了,会再到线程池中随机拿一个新的woker线程继续往下执行。也就是说我们减少了那一部分等待的时间,充份利用了线程。

    我们来对比一下使用async/awit和不使用的情况,

  不使用async/await: 20个woker线程1s可以处理200个请求。

  那转换成总的时间的就是 20 * 1000ms =  20000ms,
  其中等待的时间为 200 * 80ms = 16000ms。
  也就是说使用async/await我们至少节约了16000ms的时间,这20个worker线程又会再去处理请求,即使按照每个请求100ms的处理时间我们还可以再增加160个请求。而且别忘了100ms是基于同步情况下,包括等待时间在内的基础上得到的,所以实际情况可能还要多,当然我们这里没有算上线程切换的时间,所以实际情况中是有一点差异的,但是应该不会很大,因为我们的线程都是基于线程池的操作。
  所有结果是20个Worker线程不使用异步的情况下,1s能自理200个请求,而使用异步的情况下可以处理360个请求,立马提升80%呀!采用异步之后,对于同样的请求数量,需要的Worker线程数据会大大减少50%左右,一个线程至少会在堆上分配1M的内存,如果是1000个线程那就是1G的容量,虽然内存现在便宜,但是省着总归是好的嘛,而且更少的线程是可以减少线程池在维护线程时产生的CPU消耗的。另:dudu分享CLR1秒之内只能创建2个线程。

  注意:以上数据并非真实测试数据,真实情况一个request的时间也并非100ms,花费在web service上的时间也并非80ms,仅仅是给大家一个思路:),所以这里面用了async和await之后对响应能力有多大的提升和我们原来堵塞在这些IO和网络上的时间是有很大的关系的。

几点建议

  看到这里,不知道大家有没有得到点什么。首先第一点我们要知道的是async/await不是万能药,不们不能指望光写两个光键字就希望性能的提升。要记住, 一个CPU在同一时间段内是只能执行一个线程的。所以这也是为什么async和await建议在IO或者是网络操作的时候使用。我们的MVC站点访问WCF或者Web Service这种场景就非常的适合使用异步来操作。在上面的例子中80ms读取web service的时间,大部份时间都是不需要cpu操作的,这样cpu才可以被其它的线程利用,如果不是一个读取web service的操作,而是一个复杂计算的操作,那你就等着cpu爆表吧。

  第二点是,除了程序中利用异步,我们上面讲到的关于IIS的配置是很重要的,如果使用了异步,请记得把maxWorkerThreads和maxConcurrentRequestPerCPU的值调高试试。

  早期对Web service的异步编程模式APM

  讲完我们高大上的async/await之后,我们来看看这个技术很老,但是概念确依旧延续至今的Web Service。 我们这里所说的针对web service的异步编程模式不是指在服务器端的web service本身,而是指调用web service的客户端。大家知道对于web service,我们通过添加web service引用或者.net提供的生成工具就可以生成相应的代理类,可以让我们像调用本地代码一样访问web service,而所生成的代码类中对针对每一个web service方法生成3个对应的方法,比如说我们的方法名叫DownloadContent,除了这个方法之外还有BeginDownloadContent和EndDownloadContent方法,而这两个就是我们今天要说的早期的异步编程模式APM(Asynchronous Programming Model)。下面就来看看我们web service中的代码,注意我们现在的项目都是在.NET Framework3.5下实现的。

 PageContent.asmx的代码

public class PageContent : System.Web.Services.WebService
{
    [WebMethod]
    public string DownloadContent(string url)
    {
        var client = new System.Net.WebClient();
        return client.DownloadString(url);
    }
}

  注意我们web service中的DownloadContent方法调用的是WebClient的同步方法,WebClient也有异步方法即:DownloadStringAsync。但是大家要明白,不管服务器是同步还是异步,对于客户端来说调用了你这个web service都是一样的,就是得等你返回结果。

  当然,我们也可以像MVC里面的代码一样,把我们的服务器端也写成异步的。那得到好处的是那个托管web service的服务器,它的处理能力得到提高,就像ASP.NET一样。如果我们用JavaScript去调用这个Web Service,那么Ajax(Asynchronous Javascript + XML)就是我们客户端用到的异步编程技术。如果是其它的客户端呢?比如说一个CS的桌面程序?我们需要异步编程么?

当WinForm遇上Web Service

  WinForm不像托管在IIS的ASP.NET网站,会有一个线程池管理着多个线程来处理用户的请求,换个说法ASP.NET网站生来就是基于多线程的。但是,在WinForm中,如果我们不刻意使用多线程,那至始至终,都只有一个线程,称之为UI线程。也许在一些小型的系统中WinForm很少涉及到多线程,因为WinForm本身的优势就在它是独立运行在客户端的,在性能上和可操作性上都会有很大的优势。所以很多中小型的WinForm系统都是直接就访问数据库了,并且基本上也只有数据的传输,什么图片资源那是很少的,所以等待的时间是很短的,基本不用费什么脑力去考虑什么3秒之内必须将页面显示到用户面前这种问题。

  既然WinForm在性能上有这么大的优势,那它还需要异步吗?

  我们上面说的是中小型的WinForm,如果是大型的系统呢?如果WinForm只是其它的很小一部分,就像我们文章开始说的还有很多其它成千上万个手机客户端,Web客户端,平板客户端呢?如果客户端很多导致数据库撑不住怎么办? 想在中间加一层缓存怎么办?

  拿一个b2b的网站功能举例,用户可以通过网站下单,手机也可以下单,还可以通过电脑的桌面客户端下单。在下完单之后要完成交易,库存扣减,发送订单确认通知等等功能,而不管你的订单是通过哪个端完成的,这些功能我们都要去做,对吗?那我们就不能单独放在WinForm里面了,不然这些代码在其它的端里面又得全部全新再一一实现,同样的代码放在不同的地方那可是相当危险的,所以就有了我们后来的SOA架构,把这些功能都抽成服务,每种类型的端都是调用服务就可以了。一是可以统一维护这些功能,二是可以很方便的做扩展,去更好的适应功能和架构上的扩展。比如说像下面这样的一个系统。

 

  在上图中,Web端虽然也是属于我们平常说的服务端(甚至是由多台服务器组成的web群集),但是对我们整个系统来说,它也只是一个端而已。对于一个端来说,它本身只处理和用户交互的问题,其余所有的功能,业务都会交给后来台处理。在我们上面的架构中,应用层都不会直接参加真正业务逻辑相关的处理,而是放到我们更下层数据层去做处理。那么应用层主要协助做一些与用户交互的一些功能,如果手机短信发送,邮件发送等等,并且可以根据优先级选择是放入队列中稍候处理还是直接调用功能服务立即处理。

  在这样的一个系统中,我们的Web服务器也好,Winform端也好都将只是整个系统中的一个终端,它们主要的任何是用户和后面服务之间的一个桥梁。涉及到Service的调用之后,为了给用户良好的用户体验,在WinForm端,我们自然就要考虑异步的问题。 

WinForm异步调用Web Service

  有了像VS这样强大的工具为我们生成代理类,我们在写调用Web service的代码时就可以像调用本地类库一样调用Web Service了,我们只需要添加一个Web Reference就可以了。

// Form1.cs的代码

private void button1_Click(object sender, EventArgs e)
{
    var pageContentService = new localhost.PageContent();
    pageContentService.BeginDownloadContent("http://jesse2013.cnblogs.com",
        new AsyncCallback(DownloadContentCallback),
        pageContentService);
}

private void DownloadContentCallback(IAsyncResult result)
{
    var pageContentService = (localhost.PageContent)result.AsyncState;
    var msg = pageContentService.EndDownloadContent(result);
    MessageBox.Show(msg);
}

  代码非常的简单,在执行完pageContentService.BeginDownloadContent之后,我们的主线程就返回了。在调用Web service这段时间内我们的UI不会被阻塞,也不会出现“无法响应这种情况”,我们依然可以拖动窗体甚至做其它的事情。这就是APM的魔力,但是我们的callback究竟是在哪个线程中执行的呢?是线程池中的线程么?咋们接着往下看。

APM异步编程模式详解

线程问题

  接下来我们就是更进一步的了解APM这种模式是如何工作的,但是首先我们要回答上面留下来的问题,这种异步的编程方式有没有为我们开启新的线程?让代码说话:

private void button1_Click(object sender, EventArgs e)
{
    Trace.TraceInformation("Is current thread from thread pool? {0}", Thread.CurrentThread.IsThreadPoolThread ? "Yes" : "No");
    Trace.TraceInformation("Start calling web service on thread: {0}", Thread.CurrentThread.ManagedThreadId);
    var pageContentService = new localhost.PageContent();
    pageContentService.BeginDownloadContent("http://jesse2013.cnblogs.com",
        new AsyncCallback(DownloadContentCallback),
        pageContentService);
}

private void DownloadContentCallback(IAsyncResult result)
{
    var pageContentService = (localhost.PageContent)result.AsyncState;
    var msg = pageContentService.EndDownloadContent(result);

    Trace.TraceInformation("Is current thread from thread pool? {0}" , Thread.CurrentThread.IsThreadPoolThread ? "Yes" : "No");
    Trace.TraceInformation("End calling web service on thread: {0}, the result of the web service is: {1}",
        Thread.CurrentThread.ManagedThreadId,
        msg);
}

  我们在按钮点击的方法和callback方法中分别输出当前线程的ID,以及他们是否属于线程池的线程,得到的结果如下:

  Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? NO
  Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? NO
  Desktop4.0.vshost.exe Information: 0 : Start calling web service on thread: 9
  Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? YES
  Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? YES
  Desktop4.0.vshost.exe Information: 0 : End calling web service on thread: 14, the result of the web service is: <!DOCTYPE html>...

  按钮点击的方法是由UI直接控制,很明显它不是一个线程池线程,也不是后台线程。而我们的callback却是在一个来自于线程池的后台线程执行的,答案揭晓了,可是这会给我们带来一个问题,我们上面讲了只有UI线程也可以去更新我们的UI控件,也就是说在callback中我们是不能更新UI控件的,那我们如何让更新UI让用户知道反馈呢?答案在后面接晓 :),让我们先专注于把APM弄清楚。

从Delegate开始

  其实,APM在.NET3.5以前都被广泛使用,在WinForm窗体控制中,在一个IO操作的类库中等等!大家可以很容易的找到搭配了Begin和End的方法,更重要的是只要是有代理的地方,我们都可以使用APM这种模式。我们来看一个很简单的例子:

delegate void EatAsync(string food);
private void button2_Click(object sender, EventArgs e)
{
    var myAsync = new EatAsync(eat);
    Trace.TraceInformation("Activate eating on thread: {0}", Thread.CurrentThread.ManagedThreadId);
    myAsync.BeginInvoke("icecream", new AsyncCallback(clean), myAsync);
}

private void eat(string food)
{
    Trace.TraceInformation("I am eating.... on thread: {0}", Thread.CurrentThread.ManagedThreadId);
}

private void clean(IAsyncResult asyncResult)
{
    Trace.TraceInformation("I am done eating.... on thread: {0}", Thread.CurrentThread.ManagedThreadId);
}

  上面的代码中,我们通过把eat封装成一个委托,然后再调用该委托的BeginInvoke方法实现了异步的执行。也就是实际的eat方法不是在主线程中执行的,我们可以看输出的结果:

  Desktop4.0.vshost.exe Information: 0 : Activate eating on thread: 10
  Desktop4.0.vshost.exe Information: 0 : I am eating.... on thread: 6
  Desktop4.0.vshost.exe Information: 0 : I am done eating.... on thread: 6

  clean是我们传进去的callback,该方法会在我们的eat方法执行完之后被调用,所以它会和我们eat方法在同一个线程中被调用。大家如果熟悉代理的话就会知道,代码实际上会被编译成一个类,而BeginInvoke和EndInvoke方法正是编译器为我们自动加进去的方法,我们不用额外做任何事情,这在早期没有TPL和async/await之前(APM从.NET1.0时代就有了),的确是一个不错的选择。

再次认识APM

了解了Delegate实现的BeginInvoke和EndInvoke之后,我们再来分析一下APM用到的那些对象。 拿我们Web service的代理类来举例,它为我们生成了以下3个方法:

  1. string DownloadContent(string url): 同步方法
  2. IAsyncResult BeginDownloadContent(string url, AsyncCallback callback, object asyncState): 异步开始方法
  3. EndDownloadContent(IAsyncResult asyncResult):异步结束方法

  在我们调用EndDownloadContent方法的时候,如果我们的web service调用还没有返回,那这个时候就会用阻塞的方式去拿结果。但是在我们传到BeginDownloadContent中的callback被调用的时候,那操作一定是已经完成了,也就是说IAsyncResult.IsCompleted = true。而在APM异步编程模式中Begin方法总是返回IAsyncResult这个接口的实现。IAsyncReuslt仅仅包含以下4个属性:

  WaitHanlde通常作为同步对象的基类,并且可以利用它来阻塞线程,更多信息可以参考 MSDN 。 借助于IAsyncResult的帮助,我们就可以通过以下几种方式去获取当前所执行操作的结果。

  1. 轮询
  2. 强制等待
  3. 完成通知

  完成通知就是们在"WinForm异步调用WebService"那结中用到的方法,调完Begin方法之后,主线程就算完成任务了。我们也不用监控该操作的执行情况,当该操作执行完之后,我们在Begin方法中传进去的callback就会被调用了,我们可以在那个方法中调用End方法去获取结果。下面我们再简单说一下前面两种方式。

//轮询获取结果代码

var pageContentService = new localhost.PageContent();
IAsyncResult asyncResult = pageContentService.BeginDownloadContent("http://jesse2013.cnblogs.com",
    null,
    pageContentService);

while (!asyncResult.IsCompleted)
{
    Thread.Sleep(100);
}
var content = pageContentService.EndDownloadContent(asyncResult);

  // 强制等待结果代码

var pageContentService = new localhost.PageContent();
IAsyncResult asyncResult = pageContentService.BeginDownloadContent("http://jesse2013.cnblogs.com",
    null,
    pageContentService);

// 也可以调用WaitOne()的无参版本,不限制强制等待时间
if (asyncResult.AsyncWaitHandle.WaitOne(2000))
{
    var content = pageContentService.EndDownloadContent(asyncResult);
}
else
{ 
    // 2s时间已经过了,但是还没有执行完   
}

EAP(Event-Based Asynchronous Pattern)

  EAP是在.NET2.0推出的另一种过渡的异步编程模型,也是在.NET3.5以后Microsoft支持的一种做法,为什么呢? 如果大家建一个.NET4.0或者更高版本的WinForm项目,再去添加Web Reference就会发现生成的代理类中已经没有Begin和End方法了,记住在3.5的时候是两者共存的,你可以选择任意一种来使用。但是到了.NET4.0以后,EAP成为了你唯一的选择。(我没有尝试过手动生成代理类,有兴趣的同学可以尝试一下)让我们来看一下在.NET4下,我们是如何异步调用Web Service的。

private void button1_Click(object sender, EventArgs e)
{
    var pageContent = new localhost.PageContent();
    pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com");
    pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted;
}

private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e)
{
    if (e.Error == null)
    {
        textBox1.Text = e.Result;
    }
    else
    { 
        // 出错了
    }
}

线程问题

  不知道大家还是否记得,在APM模式中,callback是执行在另一个线程中,不能随易的去更新UI。但是如果你仔细看一下上面的代码,我们的DownloadContentCompleted事件绑定的方法中直接就更新了UI,把返回的内容写到了一个文本框里面。通过同样的方法可以发现,在EAP这种异步编程模式下,事件绑定的方法也是在调用的那个线程中执行的。也就是说解决了异步编程的时候UI交互的问题,而且是在同一个线程中执行。 看看下面的代码:

private void button1_Click(object sender, EventArgs e)
{
    Trace.TraceInformation("Call DownloadContentAsync on thread: {0}", Thread.CurrentThread.ManagedThreadId);
    Trace.TraceInformation("Is current from thread pool? : {0}", Thread.CurrentThread.IsThreadPoolThread ? "YES" : "NO");

    var pageContent = new localhost.PageContent();
    pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com");
    pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted;
}

private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e)
{
    Trace.TraceInformation("Completed DownloadContentAsync on thread: {0}", Thread.CurrentThread.ManagedThreadId);
    Trace.TraceInformation("Is current from thread pool? : {0}", Thread.CurrentThread.IsThreadPoolThread ? "YES" : "NO");
}

   Desktop4.vshost.exe Information: 0 : Call DownloadContentAsync on thread: 10
  Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO
  Desktop4.vshost.exe Information: 0 : Completed DownloadContentAsync on thread: 10
  Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO

async/await 给WinFrom带来了什么

  如果说async给ASP.NET带来的是处理能力的提高,那么在WinForm中给程序员带来的好处则是最大的。我们再也不用因为要实现异步写回调或者绑定事件了,省事了,可读性也提高了。不信你看下面我们将调用我们那个web service的代码在.NET4.5下实现一下:

private async void button2_Click(object sender, EventArgs e)
{
    var pageContent = new localhost.PageContentSoapClient();
    var content = await pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com");

    textBox1.Text = content.Body.DownloadContentResult;
}

  简单的三行代码,像写同步代码一样写异步代码,我想也许这就是async/await的魔力吧。在await之后,UI线程就可以回去响应UI了,在上面的代码中我们是没有新线程产生的,和EAP一样拿到结果直接就可以对UI操作了。

  async/await似乎真的很好,但是如果我们await后面的代码执行在另外一个线程中会发生什么事情呢?

private async void button1_Click(object sender, EventArgs e)
{
    label1.Text = "Calculating Sqrt of 5000000";
    button1.Enabled = false;
    progressBar1.Visible = true;

    double sqrt = await Task<double>.Run(() =>
    {
        double result = 0;
        for (int i = 0; i < 50000000; i++)
        {
            result += Math.Sqrt(i);

            progressBar1.Maximum = 50000000;
            progressBar1.Value = i;
        }
        return result;
    });

    progressBar1.Visible = false;
    button1.Enabled = true;
    label1.Text = "The sqrt of 50000000 is " + sqrt;
}

  我们在界面中放了一个ProgressBar,同时开一个线程去把从1到5000000的平方全部加起来,看起来是一个非常耗时的操作,于是我们用Task.Run开了一个新的线程去执行。(注:如果是纯运算的操作,多线程操作对性能没有多大帮助,我们这里主要是想给UI一个进度显示当前进行到哪一步了。)看起来没有什么问题,我们按F5运行吧!
  Bomb~

  当执行到这里的时候,程序就崩溃了,告诉我们”无效操作,只能从创建porgressBar的线程访问它。“  这也是我们一开始提到的,在WinForm程序中,只有UI主线程才能对UI进行操作,其它的线程是没有权限的。接下来我们就来看看,如果在WinForm中实现非UI线程对UI控制的更新操作。 

不同线程之间通讯的问题

万能的Invoke

  WinForm中绝大多数的控件包括窗体在内都实现了 Invoke方法,可以传入一个Delegate,这个Delegate将会被拥有那个控制的线程所调用,从而避免了跨线程访问的问题。

Trace.TraceInformation("UI Thread : {0}", Thread.CurrentThread.ManagedThreadId);
double sqrt = await Task<double>.Run(() =>
{
    Trace.TraceInformation("Run calculation on thread: {0}", Thread.CurrentThread.ManagedThreadId);
    double result = 0;
    for (int i = 0; i < 50000000; i++)
    {
        result += Math.Sqrt(i);
        progressBar1.Invoke(new Action(() => {
            Trace.TraceInformation("Update UI on thread: {0}", Thread.CurrentThread.ManagedThreadId);
            progressBar1.Maximum = 50000000;
            progressBar1.Value = i;
        }));
    }
    return result;
});

  Desktop.vshost.exe Information: 0 : UI Thread : 9
  Desktop.vshost.exe Information: 0 : Run calculation on thread: 10
  Desktop.vshost.exe Information: 0 : Update UI on thread: 9

  Invoke方法比较简单,我们就不做过多的研究了,但是我们要考虑到一点,Invoke是WinForm实现的UI跨线程沟通方式,WPF用的却是Dispatcher,如果是在ASP.NET下跨线程之间的同步又怎么办呢。为了兼容各种技术平台下,跨线程同步的问题,Microsoft在.NET2.0的时候就引入了我们下面的这个对象。

SynchronizationContext上下文同步对象

为什么需要SynchronizationContext

  就像我们在WinForm中遇到的问题一样,有时候我们需要在一个线程中传递一些数据或者做一些操作到另一个线程。但是在绝大多数情况下这是不允许的,出于安全因素的考虑,每一个线程都有它独立的内存空间和上下文。因此在.NET2.0,微软推出了SynchronizationContext。

  它主要的功能之一是为我们提供了一种将一些工作任务(Delegate)以队列的方式存储在一个上下文对象中,然后把这些上下文对象关联到具体的线程上,当然有时候多个线程也可以关联到同一个SynchronizationContext对象。获取当前线程的同步上下文对象可以使用SynchronizationContext.Current。同时它还为我们提供以下两个方法Post和Send,分别是以异步和同步的方法将我们上面说的工作任务放到我们SynchronizationContext的队列中。

SynchronizationContext示例

  还是拿我们上面Invoke中用到的例子举例,只是这次我们不直接调用控件的Invoke方法去更新它,而是写了一个Report的方法专门去更新UI。

double sqrt = await Task<double>.Run(() =>
{
    Trace.TraceInformation("Current thread id is:{0}", Thread.CurrentThread.ManagedThreadId);

    double result = 0;
    for (int i = 0; i < 50000000; i++)
    {
        result += Math.Sqrt(i);
        Report(new Tuple<int, int>(50000000, i));
    }
    return result;
});

  每一次操作完之后我们调用一下Report方法,把我们总共要算的数字,以及当前正在计算的数字传给它就可以了。接下来就看我们的Report方法了。

private SynchronizationContext m_SynchronizationContext;
private DateTime m_PreviousTime = DateTime.Now;

public Form1()
{
    InitializeComponent();
    // 在全局保存当前UI线程的SynchronizationContext对象
    m_SynchronizationContext = SynchronizationContext.Current;
}

public void Report(Tuple<int, int> value)
{
    DateTime now = DateTime.Now;
    if ((now - m_PreviousTime).Milliseconds > 100)
    {
        m_SynchronizationContext.Post((obj) =>
        {
            Tuple<int, int> minMax = (Tuple<int, int>)obj;
            progressBar1.Maximum = minMax.Item1;
            progressBar1.Value = minMax.Item2;
        }, value);

        m_PreviousTime = now;
    }
}

  整个操作看起来要比Inovke复杂一点,与Invoke不同的是SynchronizationContext不需要对Control的引用,而Invoke必须先得有那个控件才能调用它的Invoke方法对它进行操作。

小结

  这篇博客内容有点长,不知道有多少人可以看到这里:)。最开始我只是想写写WinFrom下异步调用Web Service的一些东西,在一开始这篇文件的题目是”异步编程在WinForm下的实践“,但是写着写着发现越来越多的迷团没有解开,其实都是一些老的技术以前没有接触和掌握好,所以所幸就一次性把他们都重新学习了一遍,与大家分享。

  我们再来回顾一下文章所涉及到的一些重要的概念:

  1. async/await 在ASP.NET做的最大贡献(早期ASP.NET的异步开发模式同样也有这样的贡献),是在访问数据库的时候、访问远程IO的时候及时释放了当前的处理性程,可以让这些线程回到线程池中,从而实现可以去处理其它请求的功能。
  2. 异步的ASP.NET开发能够在处理能力上带来多大的提高,取决于我们的程序有多少时间是被阻塞的,也就是那些访问数据库和远程Service的时间。
  3. 除了将代码改成异步,我们还需要在IIS上做一些相对的配置来实现最优化。
  4. 不管是ASP.NET、WinForm还是Mobile、还是平板,在大型系统中都只是一个与用户交互的端而已,所以不管你现在是做所谓的前端(JavaScript + CSS等),还是所谓的后端(ASP.NET MVC、WCF、Web API 等 ),又或者是比较时髦的移动端(IOS也好,Andrioid也罢,哪怕是不争气的WP),都只是整个大型系统中的零星一角而已。当然我并不是贬低这些端的价值,正是因为我们专注于不同,努力提高每一个端的用户体验,才能让这些大型系统有露脸的机会。我想说的是,在你对现在技术取得一定的成就之后,不要停止学习,因为整个软件架构体系中还有很多很多美妙的东西值得我们去发现。
  5. APM和EAP是在async/await之前的两种不同的异步编程模式。
  6. APM如果不阻塞主线程,那么完成通知(回调)就会执行在另外一个线程中,从而给我们更新UI带来一定的问题。
  7. EAP的通知事件是在主线程中执行的,不会存在UI交互的问题。
  8. 最后,我们还学习了在Winform下不同线程之间交互的问题,以及SynchronizationContext。
  9. APM是.NET下最早的异步编程方法,从.NET1.0以来就有了。在.NET2.0的时候,微软意识到了APM的回调函数中与UI交互的问题,于是带来了新的EAP。APM与EAP一直共存到.NET3.5,在.NET4.0的时候微软带来了TPL,也就是我们所熟知的Task编程,而.NET4.5就是我们大家知道的async/await了,可以看到.NET一直在不停的进步,加上最近不断的和开源社区的合作,跨平台等特性的引入,我们有理由相信.NET会越走越好。

  最后,这篇文章从找资料学习到写出来,差不多花了我两个周未的时间,希望能够给需要的人或者感兴趣想要不断学习的人一点帮助(不管是往前学习,还是往后学习)最后还要感谢@田园里面的蟋蟀,在阅读的时候给我找了一些错别字!

引用 & 扩展阅读

http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx
http://blog.stevensanderson.com/2008/04/05/improve-scalability-in-aspnet-mvc-using-asynchronous-requests
http://blogs.msdn.com/b/tmarq/archive/2007/07/21/asp-net-thread-usage-on-iis-7-0-and-6-0.aspx 
http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx
http://mohamadhalabi.com/2014/05/08/thread-throttling-in-iis-hosted-wcf-sync-vs-async/
Pro Asynchronous Programs with .NET by Richard Blewett and Andrew Clymer

安卓邀请追踪技术和iOS渠道追踪和来源统计的几种原理 - neveraway1993的博客 - CSDN博客

$
0
0

在开始之前,我们先来看看安卓的渠道统计。Google官方的应用商店Google Play在国内一直是无法使用的状态,所以国内的安卓App分发,都是依托数十个不同的应用市场或发行渠道,如百度、360、腾讯等互联网企业以及小米、华为、魅族等手机生产商。对于安卓App的渠道追踪,主要是围绕上面这些大的渠道来进行,并且这些渠道自己一般也会提供非常详尽和周全的数据分析给应用开发者。

iOS的发行渠道则与安卓有很大的不同,除了少数越狱的机器之外,大部分用户的App都是从App Store下载的。iOS的“渠道”其实通常是指那些在其它App或者网页内部,提供到AppStore的链接的页面。因此,在iOS中追踪发行渠道,主要是追踪进入App Store相关页面的渠道信息。

但iOS的渠道追踪面临着一道无法逾越的鸿沟。正因为iOS的渠道分发都有跳转到App Store这一步,而Apple本身是不会提供太多信息给开发者,所以,对于整个流程的三个步骤:在某个渠道点击下载链接并跳转到App Store ---> App Store内下载App --->用户激活App,这其中的第二步,开发者无法获取相关信息,所以,没有办法精确地追踪一个用户在这三个步骤中的完整轨迹,也即没有办法精确地衡量渠道的具体推广效果。同时,安卓渠道效果分析中,常见的对于不同渠道打不同包的方案,在iOS分发时也是不可行的。

对于iOS的困境,该如何解决呢?现在市场上大概有以下三种方式:

通过IDFA进行追踪:这个方案一般用在App里面打开下载链接这种推广方式。基本的方案是,推广渠道的App(例如微信),会详细记录哪个IDFA点击了待推广App(例如聚美)的链接(或是在微信中嵌入SDK去记录),而聚美本身,也会记录具体的哪个IDFA激活了聚美App,两者都将记录下来的IDFA上传至指定的服务器,进行对比,即可确定下载来源。在用户不重置系统,不还原广告的情况下,这种方式精准度比较高。

通过模糊特征匹配的方式来进行追踪:点击下载链接,会跳转到appstore页面,这个过程会触发一个服务端的请求,服务器来记录这次点击的设备信息,包括ip地址、机型等。同时,被推广App这边,也可以记录用户激活App时机器的一些基本信息,并上传至服务器。结合下载和激活的时间差,再结合设备的IP地址和机型等信息,大概可以模糊地识别出同一个用户先点击了下载链接,再激活了App,从而确定下载渠道。这种方式的精确度较低。

通过SFSafariViewController进行追踪:iOS 9中新增的SFSafariViewController,这个类的API允许在app内打开一个safari浏览器,而不是一个app内部的webview。这个app内的safari和外面系统的safari是同一个,共享同一个沙盒,可以操作同一个Cookie,也就是说它可以跨App与Safari实现共享Cookie。

 基于SFSafariViewController控件,当用户在App中通过它打开渠道页面时,我们可以将渠道信息写入Cookie中,并设置生效时间。当用户安装并激活App后,再次使用SFSafariViewController上报激活信息,同时将Cookie中的渠道信息上传,通过匹配,便可确定下载来源。由于渠道信息保存在设备本地,因此匹配是100%准确的。

但是基于SFSafariViewController这种方式也有一定的弊端。首先,这个方案只能支持iOS9及以上版本的设备,大约占全部苹果设备的85%左右,覆盖了绝大部分用户,已经具有很好的分析价值了。但对于剩余的15%的用户,该方案无法满足。此外,对于目前业界主流的一些推广渠道,如微信、朋友圈,它们尚未在App中使用SFSafariViewController控件访问网页,因此这部分渠道也无法使用精准匹配的方案。

市面上的做法有的是上述三种方式单一出现,有的是两两组合,总之不管是通过哪种方式,这都是我们想象出来的间接的方式,只能说是尽量的去接近准确,但不能做到100%准确。在今年的4月15日,苹果低调发布了一项重大功能,开始提供渠道来源的数据,就以往而言,苹果仅开放有限的数据统计,很容易让从业人员在工作遇到窘境,我们该如何统计到来源渠道。而此次推出的用户来源统计,对App推广人员来说,无疑是一项重大举措。

关于苹果推出的这项功能,我会在另一篇文章中 http://blog.csdn.net/neveraway1993/article/details/72461438,详细介绍,欢迎大家前来探讨!


每次分享时生成一个带参数的url,只要想办法在app首次启动时恢复这个参数即可,恢复的方法大致有以下几种:

1.根据ip与user-agent中的设备信息做匹配,访问url时服务器记录ip与设备信息,app首次启动再去请求服务器匹配一次

2.ios9开始,可以利用cookie来跟踪,使用safari访问url时写入cookie,app首次启动时使用SFSafariViewController控件访问同一个域名,这个控件会将之前写入的cookie一并带给服务器

3.更暴力的方法,下载时将信息直接写入安装包中;android下生成一个新的apk;ios下可利用企业证书签名,通过ad-hoc分发的方式,实时生成一个新的ipa文件,不过自ios8开始,苹果对企业证书有了更严格的限制,用户体验不好

4.还见过更奇葩的android实现方法,下载apk时将参数放到apk本地文件名中(通过http头部,Content-Disposition:attachment;filename=xxxx),安装后启动app再去想办法读取这个下载记录,不过基本不靠谱




Nessus漏洞扫描教程之配置Nessus - yxwkaifa - 博客园

$
0
0

Nessus漏洞扫描教程之配置Nessus

配置Nessus

当安装成功Nessus工具后。就可以使用该工具实施漏洞扫描。为了使用户更好的使用该工具,将介绍一下该工具的相关设置。如服务的启动、软件更新、用户管理等。本节将对Nessus服务配置进行简介。

启动Nessus服务

Nessus服务安装后。默认是自己主动启动的。假设用户重新启动系统,获取进行其他操作时。将Nessus服务关闭的话。则再次訪问必需要先启动该服务。

以下将分别介绍在不同操作系统中,启动Nessus服务的方法。

1.Windows下启动Nessus服务

在Windows下启动Nessus服务的方法例如以下所看到的:

(1)打开Windows系统的服务窗体。在Windows系统的启动菜单条中单击“执行”命令,将弹出“执行”对话框,如图1.22所看到的。


图1.22  执行对话框

(2)在该对话框中输入“services.msc”,然后单击“确定”button,将打开“服务”窗体,如图1.23所看到的。


图1.23  服务窗体

(3)在该界面的名称列找到“Tenable Nessus”服务。就可以管理该服务,如停止、启动或又一次启动等。

在Windows中,也能够通过命令行停止或启动Nessus服务。比如。停止Nessus服务。运行命令例如以下所看到的:

  • C:\Users\Administrator>net stop "Tenable Nessus"
  • Tenable Nessus 服务正在停止.
  • Tenable Nessus 服务已成功停止。

从以上输出信息中,能够看到Nessus服务已成功停止。假设启动Nessus服务,运行命令例如以下所看到的:

  • C:\Users\Administrator>net start "Tenable Nessus"
  • Tenable Nessus 服务正在启动 .
  • Tenable Nessus 服务已经启动成功。

从以上输出信息中,能够看到Nessus服务已成功启动。

2.Linux下启动Nessus服务

在Linux下启动Nessus服务。运行命令例如以下所看到的:

  • [root@Server ~]# service nessusd start
  • 启动 Nessus 服务:                                         [确定]

从以上输出信息中,能够看到Nessus服务已成功启动。

假设用户不确定该服务是否启动的话。能够使用下面命令查看其状态。例如以下所看到的:

  • [root@Server ~]# service nessusd status
  • nessusd (pid 5948) 正在执行...

从以上输出信息中,能够看到Nessus服务正在执行。

Nessus软件更新

为了可以使用Nessus进行一个成功的漏洞扫描。在扫描之前检查而且更新Nessus,使用最新的插件是很重要的。这样可以保证扫描到全部最新的漏洞。以下将以Windows操作系统为例,介绍更新插件的方法。

1.在线更新

【演示样例1-3】在Windows下更新Nessus中的插件。详细操作过程例如以下所看到的:

(1)登录Nessus服务。在Windows中的浏览器地址栏输入https://IP:8834/地址。将打开如图1.24所看到的的界面。


图1.24  证书不被信任

(2)在该界面选择“继续浏览此站点(不推荐)”选项。将打开如图1.25所看到的的界面。


图1.25  登录界面   图1.26  Nessus登录界面

(3)在该界面输入用于管理Nessus服务的username和password。然后。单击Sign Inbutton。

登录成功后。将显示如图1.26所看到的的界面。

(4)在该界面单击右上角后面的小三角。将会弹出一个菜单条,如图1.27所看到的。在该菜单条中,单击Settings命令,将打开设置界面。如图1.28所看到的。


图1.27  菜单条                                       图1.28  设置界面

(5)从该界面左側栏中,能够看到有两个子选项,即Overview(概述)和Software Update(软件更新)选项。图1.28中,显示的是Overview选项中的信息。

当中。包含Nessus版本号、连接时间、平台、近期更新时间、激活码等。假设要进行软件更新,则选择Software Update选项,将显示如图1.29所看到的的界面。


图1.29  软件更新 图1.30  手动更新软件

(6)从该界面能够看到在Automatic Updates(自己主动更新)以下有三种更新方式,各自是 Update all components(更新全部组件)、Update plugins(更新插件)和Disabled(禁止更新)。用户能够选择不论什么一种更新方式。并且,Nessus还提供了一种自己定义插件更新方式。用于针对特定的主机。比如。更新IP地址为192.168.1.100主机提供的插件。则在Custom Host相应的文本框中输入地址192.168.1.100。假设用户不希望自己主动更新的话。还能够进行手动更新。在该界面单击右上角的Manual Software Update(手动更新)button,将显示如图1.30所看到的的界面。

(7)这里也提供了三种更新方式,各自是Update all components(更新全部组件)、Update plugins(更新插件)和Upload your own plugin archive(上传自己的插件文档)。用户选择想要的更新方式后。单击Continuebutton,就可以開始更新。更新完毕后。右上角(铃铛)图标处会提示更新成功,如图1.31所看到的。


图1.31  软件更新成功    图1.32  生成挑战码 

2.离线更新

以上的更新方式属于在线更新。

使用这样的方式更新的话。必需要确定自己的网络一直处于正常状态。假设用户不能确认自己网络的话能够使用离线更新方式。

这样的方式不需要Nessus系统连接必须连接到互联网。以下将介绍离线更新的方式。

【演示样例1-4】以下将以Windows 7操作系统为例,介绍离线更新插件的方法。

(1)获取一个激活码。因为获取的激活码,仅仅能使用一次。所以。假设再次激活服务。须要又一次获取一个激活码。

(2)生成一个挑战码。运行命令例如以下所看到的:

  • C:\Program Files\Tenable\Nessus> nessuscli.exe fetch --challenge

运行以上命令后,显示效果如图1.32所看到的。

提示:假设是在Linux系统中的话,运行命令例如以下所看到的:

  • [root@localhost ~]# /opt/nessus/sbin/nessuscli fetch --challenge

(3)从上图中能够看到生成了一个激活码。接下来,就能够离线下载Nessus插件了。

当中,下载地址为https://plugins.nessus.org/v2/offline.php。在浏览器中成功訪问该地址后,将显示如图1.33所看到的的界面。

(4)在该界面的第一行文本框中输入步骤(2)中获取到的挑战码,第二行文本框中输入获取到的激活码。然后,单击Submitbutton。就可以開始下载插件。

在该界面获取到的是6.3及更新的插件。假设用户想要获取版本号为6.3之前插件的话,在单击图中箭头指的here命令,将会跳转到还有一个页面,如图1.34所看到的。


图1.33  离线下载插件    图1.34  下载旧版本号的插件

(5)该界面和图1.31显示的内容是一样的。这里相同输入生成的挑战码和激活码。就可以获取旧版本号的插件。

Nessus中用户管理

用户管理是Nessus额外提供的一种功能。在一个大型企业环境中。或使用Nessus的人比較多时,对用户进行管理是很实用的。当在这样的情况下使用Nessus扫描时,管理员能够为多个扫描用户设置不同的安全级别。

Nessus提供了两种不同的用户角色,各自是Administrator(管理员)和Standard(普通用户)。

当中,Administrator角色的用户能够訪问Nessus中的全部功能;Standard角色的用户对部分功能是受限制的,如软件更新、用户管理及高级设置等。以下将介绍对Nessus中用户管理的方法。

1.新建用户

在Nessus的设置界面选择Accounts选项卡,将显示如图1.35所看到的的界面。


图1.35  账户设置界面 图1.36  新建用户

在该界面单击右上角的New Userbutton,将打开如图1.36所看到的的界面。

在该界面输入要创建的username和password。User Role相应的文本框有两个选项。各自是Standard和System Administrator。当中。Standard选项表示创建的用户为普通用户;System Administrator选项表示创建的用户为管理员用户。然后单击Savebutton,将看到如图1.37所看到的的界面。


图1.37  用户界面   图1.38  删除用户

从该界面能够看到成功创建了名为user用户,类型为Standard。

2.删除用户

当Nessus扫描不须要某用户时,就可以将该用户删除。详细方法例如以下所看到的:

(1)打开用户设置界面,如图1.31所看到的。

(2)在该界面选择要删除的用户,然后,单击username后面的(错号)图标就可以删除用户。或者,勾选username前面的复选框。

此时,在搜索框的左側将会出现一个Deletebutton,如图1.38所看到的。然后,单击Deletebutton,将显示如图1.39所看到的的界面。


  图1.39  确认删除用户 图1.40  编辑用户界面

该界面提示是否确定要删除该用户。

假设确认没问题。则单击Deletebutton,就可以成功删除该用户。

3.改动已存在用户角色

在用户界面(图1.31)中单击要改动角色的用户。就可以改变用户的角色。比如。编辑user用户。在用户界面单击user用户后。将显示如图1.40所看到的的界面。

从该界面能够看到user用户的角色为Standard。这里单击User Role相应文本框后面的小三角。就可以选择要改动角色。

比如。改动为System Administrator角色。将显示如图1.41所看到的的界面。


图1.41  改动用户角色 图1.42  改动password

此时,用户角色已成功改动。接下来。须要单击Savebutton保存设置。

否则,设置无效。

4.改动用户password

改动password也是在用户设置界面改动的。相同,单击想要改动password的用户。然后。单击左側栏中的Change Password选项卡,将显示如图1.42所看到的的界面。

在该界面输入要又一次设置的新password。然后单击Savebutton。就可以成功改动其用户password。

Nessus中通讯设置

这里的通讯设置指的是设置选项中的Communication选项卡。在该选项卡设置中,包含两个设置选项,各自是Proxy Server和SMTP Server。以下分别介绍这两种服务的设置方式。


图1.43  Proxy Server设置界面   图1.44  SMTP服务设置界面 

1.Proxy服务

Proxy(代理)服务用于转发HTTP请求。假设网络组织须要时。Nessus将使用该设置实现插件更新,并与远程扫描者进行通信。以下将介绍Proxy服务的设置方法。

例如以下所看到的:

(1)在设置界面选择Communication选项卡,将显示如图1.43所看到的的界面。

从该界面能够看到,这里共同拥有五个字段。可是,仅仅有Host和Port字段是必须的。Username、Password和User-Agent三个字段是可选的。以下将分别介绍每一个字段的含义,例如以下所看到的:

  • q  Host:代理server的主机名名或IP。
  • q  Port:代理server连接的port号。

  • q  Username:代理server连接的username。
  • q  Password:代理server连接的usernamepassword。

  • q  User-Agent:假设代理server使用指定HTTP用户代理过滤器的话,则设置该字段。

    该字段主要用于自己定义用代理字符串时使用。

2. SMTP服务

SMTP(Simple Mail Transfer Protocol。简单邮件传输协议)是用于发送和接收邮件的标准。一旦配置了SMTP服务。Nessus会将扫描结果通过邮件的形式发送到“Email Notifications”选项指定的收件人。当中,SMTP服务的设置界面如图1.44所看到的。

以下将对SMTP服务设置界面的每一个字段进行具体介绍。

例如以下所看到的:

  • q  Host:SMTP服务的主机名或IP地址。
  • q  Port:用于连接SMTP服务的port号。
  • q  From(sender email):发送扫描报告的邮件地址。

  • q  Encryption:使用哪种加密方式加密邮件内容。Nessus提供了三种方式,各自是Force SSL、Force TLS和Use TLS if available。默认。不使用加密(No Encryption)。
  • q  Hostname(for email links):Nessus服务的主机名或IP地址。

  • q  Auth Method:SMTP服务认证方法。

    Nessus提供了五种认证方法,各自是PLAIN、LOGIN、NTLM和CRAM-MD5。默认,没有使用认证方法,即NONE。

  • q  Username:用于认证SMTP服务的username
  • q  Password:用于认证SMTP服务用户相应的password。

提示:在SMTP服务设置界面,假设没有使用不论什么认证方法的话,将不会出现Username和Password字段。

本文选自:Nessus漏洞扫描基础教程大学霸内部资料,转载请注明出处,尊重技术尊重IT人!

人脸相关数据库 - marleylee的博客 - CSDN博客

$
0
0

在人脸检测、人脸识别和属性分析等方面,常用的 数据库可分为以下五部分。

1、人脸检测数据库:

(1999年发布)CMU+MIT:180幅图像,共734个人脸。包含3个正面人脸 测试子集和一个旋转人脸测试子集,其中正面人脸测试子集有130幅图像,共511个人脸;旋转人脸测试子集有50幅图像,共223个人脸。


(2010年发布)FDDB:2845幅图像,共5171个人脸。

(2012年发布)AFW:205幅图像,共468个人脸。由从Flickr采集的205幅图像组成,共468个人脸,其包含复杂的背景变化和人脸姿态变化等。


(2015年发布)MALF: 5250幅图像,共11931个人脸。
(2015年发布)IJB-A:24327幅图像,共49759个人脸
(2016年发布)WIDER:32203幅图像,共393703个人脸

2、人脸关键点检测数据库:

(2001年发布)BioID :约1000幅图像,每个人脸标定20个关键点。

https://www.bioid.com/About/BioID-Face-Database

(2011年发布)LFPW:1132幅图像,每个人脸标定29个关键点

http://neerajkumar.org/databases/lfpw/

(2011年发布)AFLW:25993幅图像,每个人标定21个关键点

https://lrs.icg.tugraz.at/research/aflw/

(2013年发布)COFW:1852幅图像,每个人脸标定29个关键点

http://www.vision.caltech.edu/xpburgos/

(2014年发布)ICCV13/MVFW :2500幅图像,每个人脸标定68个关键点

https://sites.google.com/site/junliangxing/codes

(2014年发布)OCFW: 3837幅图像,每个人脸标定68个关键点

https://sites.google.com/site/junliangxing/codes

(2016年发布)300-W :600幅图像,每个人标定68个关键点

http://ibug.doc.ic.ac.uk/resources/300-W_IMAVIS/

3、人脸识别数据库:

(2004年发布)CASPEAL:约1000个人,共约3万幅人脸图像

http://www.jdl.ac.cn/peal/index.html

(2008年发布)Multi-PIE:337个人,共约75万图像

http://www.flintbox.com/public/project/4742/

(2007年发布)LFW :5749个人,共13233幅人脸图像

http://vis-www.cs.umass.edu/lfw/

(2009年发布)PubFig :200个人,共58797幅人脸图像

http://www.cs.columbia.edu/CAVE/databases/pubfig/

(2014年发布)CASIAWebFace :10575个人,共49414幅人脸图像

http://www.cbsr.ia.ac.cn/english/CASIAWebFace-Database.html

(2014年发布)FaceScrub :530个人,共106863幅人脸图像

http://vintage.winklerbros.net/facescrub.html

(2016年发布)MegaFace :约69万个人,共约100万幅人脸图像

http://megaface.cs.washington.edu/


4、人脸属性识别数据库:

(1999年发布)JAFFE:10个人,共213幅人脸图像(表情识别)

http://www.kasrl.org/jaffe.html

(2010年发布)CK+ :123个人,共593段视频(表情识别)

http://www.pitt.edu/~emotion/ck-spread.htm

(2010年发布)MMI :75个人,共2900段视频(表情识别)

http://mmifacedb.eu/

(2003年发布)FG-NET:82个人,共1002幅人脸图像(年龄识别)

http:// www-prima.inrialpes.fr/FGnet/html/benchmarks.html 

(2006年发布)MORPH:13673个人,共55608 幅图像(年龄识别)

http://www.faceaginggroup.com/morph/

(2014年发布)Adience : 2284个人,共26580幅人脸图像(年龄、性别识别)

http://www.openu.ac.il/home/hassner/Adience/data.html

(2015年发布)IMDBWIKI :20284个人,共523051幅人脸图像(年龄、性别识别)

https://data.vision.ee.ethz.ch/cvl/rrothe/imdb-wiki/

(2015年发布)CACD2000 :2000个人,共163446幅人脸图像(年龄识别)

http://bcsiriuschen.github.io/CARC/

(2015年发布)CelebA:10177个人,共202599幅人脸图像(属性识别)

http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html

5、其他数据库(活体检测):

YouTube 名人2008 47个人,共1910段视频

http://seqam.rutgers.edu/site/media/data_files/ytcelebrity.tar

YouTube 2011 1595个人,共3425段视频

http://www.cs.tau.ac.il/~wolf/ytfaces/

KFW 2012 533对亲属关系(KFW-I)和1000 对亲属关系(KFW-II)

http://www.kinfacew.com/download.html

CASIA 2012 50个人,每个人12段视频

http://www.cbsr.ia.ac.cn/english/FaceAntiSpoofDatabases.asp

Replay-Attack2012 50个人,每个人24段视频

https://www.idiap.ch/dataset/replayattack


活体检测数据库简介:

Database

Year of release

# subjects

# videos

Acquisition camera device

Attack type

Subject race

Subject gender

Subject age

NUAA [1]

2010

15

•24 genuine

• 33 spoof

•  Web-cam

(640 × 480)

• Printed photo

• Asian 100%

• Male 80%

• Female 20%

20 to 30

yrs

Idiap REPLAYATTACK [2][3][4]

2012

50

• 200 genuine

• 1,000 spoof

• MacBook 13’’  

camera (320 × 240)

• Printed photo

• Display photo

(mobile/HD)

• Replayed video

(mobile/HD)

• White 76%

• Asian 22%

• Black 2%

• Male 86%

• Female 14%

20 to 40

yrs

CASIA FASD [5]

2012

50

• 150 genuine

• 450 spoof

• Low-quality camera

(640 × 480)

• Normal-quality

camera (480  × 640)

• Sony NEX-5

camera (1280 × 720)

• Printed photo

Cut photo

• Replayed video

(HD)

• Asian 100%

• Male 86%

• Female 14%

20 to 35

yrs

MSU MFSD [6]

2014

55

• 110 genuine

• 330 spoof

• MacBook Air 13”

camera (640 × 480)

• Google Nexus 5

camera (720 × 480)

• Printed photo

• Replayed video

(mobile/HD)

• White 70%

• Asian 28%

• Black 2%

• Male 63%

• Female 37%

20 to 60

yrs

The Oulu-NPU face anti-spoofing  database

2016

55

• 990 genuine

• 3,960 spoof

• Front cameras of six mobile devices(1080×1920)

( Samsung Galaxy S6 edge,  HTC Desire EYEMEIZU X5ASUS Zenfone SelfieSony XPERIA C5 Ultra Dual and  OPPO N3)

• Two printed photo

• Two replayed video

(mobile/HD)

• White 4%

• Asian 96%

• Male 69%

•  Female31%

20 to 60

yrs

注:① MSU MFSD中55个人的数据,只有35个人的数据可以公开使用。

②“Cut photo attack”表示将打印图片中眼睛部位剪掉,攻击者用他的照片盖住他的脸,并且可以在洞里眨眼睛。


参考文献:
[1] 严严,陈日伟,王菡子.基于深度学习的人脸分析研究进展[J].厦门大学学报(自然科学版),2017,56(1):13-24.

[2]  Di Wen, Member, IEEE, Hu Han, Member, IEEE and Anil K. Jain, “Face Spoof Detection with Image Distortion Analysis”, in IEEE Transactions on Information Forensics and Security,2015,pp.1–16.

VC++开发必备神器 -- Dependencies,查看依赖库DLL,支持win10,比depends更好用 - $firecat的代码足迹$ - CSDN博客

$
0
0

1、微软官方有提供depends,可以查看exe文件的依赖库,仅适用于winxp/win7/win8,但是不能用于win10,会卡死报错.

官网下载: http://www.dependencywalker.com/ 

2、隆重推荐 Dependencies,可以应用在winxp/win7/win8/win10,推荐使用!

软件下载:

https://github.com/lucasg/Dependencies

https://github.com/lucasg/Dependencies/releases

软件依赖环境:

需要下载Microsoft Visual C++ Redistributable,The latest supported Visual C++ downloads:

https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads
最后,运行程序:

DependenciesGui.exe

3、DLL下载网站: https://www.dll-files.com/

 

那些年我们一起犯过的错 - 旁观者 - 博客园

$
0
0

阶段性小结:错误是我们的财富

对于事故处理,我们遵从航天二十字诀:定位准确、机理清楚、可以复现、措施有效、举一反三。

“丰田生产体系”与航空航天的这个原则是相通的,如果对待错误的态度是开诚布公的,那么整套系统就能从中学习,能取得进步。

我们坚持每错必查、错了又错就整改、每错必写,用身体力行告诉每一个新员工直面错误、公开技术细节、分享给所有人,长此以往,每一次事故都会变为我们的财富。

 

 

我的错误1—做好被攻陷的准备:刚接手工作几个月,黑客入侵

很多年前,没想到我刚刚到任几个月,数据库就被黑客下载了。

过程是这样的:

凌晨2:46,黑客通过某地IDC机房里的一台肉鸡,仅仅两次尝试输入管理后台登录地址后,就准确地输对了,说明此黑客清楚我们的后台登录拼写规则,否则不可能在7秒内依次尝试很难的拼写。

登入8分钟后,他利用 FCKeditor 版本2.4.2以下的PHP版上传文件漏洞,上传了多个 php 文件。

然后,他种了一个 rootkit,即在 include 文件夹下放了一个 lndex.php,浏览这个 php 就可以从网页上修改操作系统 root 帐号的密码。

总之,黑客在凌晨3点到6点之间,在那台宿主机上密密麻麻地放了很多 php 文件上来,并修改了很多系统文件。

 

几天后,黑客又从那个肉鸡来了,访问他之前在服务器上种下的 rootkit。

然后,可能利用他从服务器上代码配置文件中看到的数据库用户名和密码,备份数据库并下载。

 

尾声:

内部IT系统的后端:

-在服务器端,日志文件里不要存储用户或商户的敏感信息,如登录密码、银行卡号、身份证号,曾经有一个知名公司的工程师为了调试方便,将用户的信用卡卡号和卡密记在日志文件里,但白帽子们发现能访问到这个日志文件……

-我们认为数据库有可能被盗走,所以要做到即使被拿走,也不要给客户和用户造成损失,所以数据库存取这些商业敏感信息时需要做高强度的对称加解密。

-工程配置文件只允许存储加密后的数据库登录密码,同时部署人员和开发人员都不允许掌握明文密码。

 

内部IT系统的前端:

-登录做两步验证;

-禁止多点登录;

-从框架上做好防 XSS+SessionHijacking+CSRF+SQLi……

-我们认为第三方很有可能拿到我们的平台登录权限(通过 session hijacking,或通过内部人),所以即使在合法用户登录状态下,敏感字段的展示要有遮挡,修改或查看敏感信息的时候要输入短验验证身份;

-内部IT系统的 robots.txt 内容必须是:『User-agent: * Disallow: /』,禁止搜索引擎收录。

 

点题:

我的态度是:放眼一年、三年、五年、十年,你的系统一定会被人攻陷,你的数据一定会被人拿走,往往是几个初中级安全漏洞,再加上一次社会工程学,就能成功渗透,并不需要高危漏洞。所以要做好灾难即将来临的准备,即使被攻陷,被拿走,也不要给商户和用户带来二次伤害。

 

 

别人的错误3—工具与风控:骑士资本集团的覆灭

2012年的时候,骑士资本是美国股票市场最大的经纪商,分别占有纽交所和纳斯达克 17% 的市场份额。

骑士资本的电子贸易部门管理的平均日交易量超过 33 亿股,交易额高达210 亿美元。

截止到 2012 年 7 月 31 日,骑士资本拥有高达 3 亿6500 万美元的现金及现金等价物。

 

在8月1日之前,骑士资本按照纽交所的项目计划,更新了算法程序 SMARS,它从交易平台接收大订单,然后根据买家或卖家的股票交易数量把大订单拆分成合适的小订单。

 

这次更新去掉了一些过时的代码,如 Power Peg,虽然它已经 8年没有用过了,但实际上 Power Peg 模块一直处于待命状态,只要系统的某一个特殊的参数被设置为「YES」,该模块就会被调用用来交易。

 

程序员开发了一个新的 RLP 模块,取代之前的 Power Peg 模块。取代后,之前那个特殊的参数被设置为「YES」,意思是使用 RLP 模块。

 

听起来是不是让人担心?

 

不用担心,测试完全通过,虽然更新后的代码沿用了以前用来激活 Power Peg 模块的标识符,但代码非常可靠。

 

7月27日到7月31日,骑士资本把 SMARS 软件手动部署到公司为数不多的服务器上。

一共才 8 台。

不幸的是,漏了一台服务器。

因为没有其他技术人员对部署过程做复查,所以没有人发觉第 8 台服务器上的 Power Peg 代码并没有被移除。

所以这台服务器上并没有 RLP 模块,只有 Power Peg 模块。「Power Peg」模块在被停用后的第10年被启动了。

 

灾难正在一分一秒地迫近。

 

2012年8月1日早上9点30分开盘后,很多交易员感觉到异乎寻常的事情发生了,某些个股涌现出大量不符合常理的订单,而且没有停止的迹象。

 

这个系统竟然没有断开的开关。

 

于是乎,在 45 分钟之内,骑士资本执行了超过日均交易额 50% 的订单,导致部分股票市值上升超过 10%,带来的连锁反应是其他股票价格暴跌。

 

由于没办法断开系统,也没有相关情况的预案说明,魂飞魄散的程序员只能在每分钟交易 800 万股的生产环境里调试。

 

因为没有能在线上发现问题,所以回滚了代码。

 

情况反而恶化了。

 

原本只是第 8 台上的 Power Peg 在疯狂地工作。

现在另外 7 台服务器上的 Power Peg 也加入了进来。

 

最后,骑士资本的技术人员和纽交所一起终于想办法终止了交易系统,然而已经过去了 45 分钟。

 

灾难现场,一片狼藉。

 

在这 45 分钟里,

对于内行人来说,骑士资本建立了 80 支个股 35 亿美元的净多头仓位和 74 支个股 31 亿 5000 万美元的净空头仓位。

对外行人来说,骑士资本在 45 分钟内亏损了 4 亿 6000 万美元,而上文提到,骑士资本仅有 3亿6500万美元的资产,这意味着骑士资本破产了。

 

骑士资本集团在整个事件中犯下的错误有哪些呢?

1,Power Peg 模块在停用时并没有从系统中删除,而是保留在系统里成为僵尸程序。

2,运维工程师手工部署,没有交叉验证,操作重大失误。

3,他们的风险管理完全是事后管理,缺乏事前控制。虽然对公司的敞口设置了限额,但超过限额时交易系统无任何限制。

4,他们的风险管理工具PMON,是一个事后的风险管理工具,完全依赖于人工监控。当交易量较大时,该系统还会有延迟,产生错误的报告。所以在灾难发生的时候,业务人员没有快速定位到敞口的来源,也没有意识到问题的严重性。

 

点题:

1,工具:假定人的错误是不可避免的,上线部署就应该是自动化的,而且是可重复的过程,尽量排除人为因素的干扰。如果你常年靠手动发布,总有一天会大难临头。

2.风控:你的业务保障平台,你的风控管理系统,是你的最重要的伙伴,不要轻视它,在关键时刻,它会救你的命。

 

最后,我们再呼应一下主题:

第一,

『我得到正确判断的办法,

通常是先收集各种错误判断的例子,

然后仔细考虑怎样避免得到这些下场。』

——《穷查理宝典2》查理·芒格

 

第二,

错误是我们的财富。

我们坚持每错必查、错了又错就整改、每错必写的RCA制度,

用身体力行告诉每一个新员工直面错误、公开技术细节、分享给所有人,

长此以往,每一次事故都会变为我们的财富,而不是包袱。

 

-EOF-

关于直播视频平台与监控视频平台技术架构方案的一点小想法 - eguid - 博客园

$
0
0

javaCV入门指南:序章

截图服务在线演示demo: https://blog.csdn.net/eguid_1/article/details/82842904

项目维护地址: https://github.com/eguid/easyCV

感谢支持eguid原创,有兴趣的小伙伴可以点击博客左边的群链接加群讨论。

前言

讲个大实话,直播平台复杂在直播端(也就是播放端),而监控平台复杂在接入端(前端设备或平台)。

至于技术难点,难者自知。

 

一、直播平台(想尽一切办法来降低延迟,从一开始你就不应该对hls抱有任何幻想)
1、直播房间管理
主播管理--[主播申请直播房间]-->房间管理--[房间绑定直播推流地址]-->分配流媒体直播地址(可能会有多个流媒体服务,房间id作为rtmp和flv播放名称,只提供rtmp和flv分发,直播场景不考虑hls)

2、流媒体服务(srs或nginx+rtmpmodule),定制需求(通过主播推流和用户播放回调事件展示实时数据,用户真实数量后台不做调整,前端随意,回调接口由web服务提供)

3、前端直播(web,pc,移动端,微信小程序)
主要是播放器(h5采用flv方案,原生随意),还有实时弹幕和评论,礼物等等。
直播这块主要难点在于cdn分发降低延迟,其他没有难点。
小程序这块微信有提供单独的liveplayer播放器API,支持rtmp和flv,直接用就可以。
pc端想怎么搞怎么搞,就算你想自己解码然后播放又有什么不可以呢?
web端主要还是H5为主(flv,兼容低版本ie的话,可以上flash播放器),毕竟直播还是年轻人看的多,老年人应该会选择看电视吧~~maybe。

 

二、监控视频平台(视频接入复杂度较高,依然不考虑hls)
监控平台复杂度体现在接入复杂,接入协议多,私有协议满天飞,gb28181,177平台,sip,各种设备对接和监控系统对接,音视频裸码流,rtsp,rtmp,rtmp,hls,录像文件等等。


1、视频接入服务
需要一个统一接入服务(必须确定接入的每台设备的接入信息和接入方式(大而全,支持协议足够多,支持各种厂商私有码流,吃力而不讨好),流媒体服务会通过回调方式让本服务进行拉流并推流到流媒体服务)。


2、流媒体服务
首选srs或nginx+rtmpmodule,定制需求(即时转流,通过用户调用方式触发回调接入服务进行实时推流到流媒体服务)
既然是即时转流,那么延迟肯定比直播平台延迟要大,用户体验一般般;想要更好的用户体验就需要不停的从前端设备拉流推流到流媒体服务,后者本质上跟直播没区别。

 

3、监控视频直播(web,pc,移动端,微信小程序)
这块跟直播平台没啥区别,只不过不需要弹幕什么的了,前端比较简单,只要能看视频就行。
小程序这块微信有提供单独的liveplayer播放器API,支持rtmp和flv,直接用就可以。
pc端想怎么搞怎么搞,就算你想自己解码然后播放又有什么不可以呢?

 

三、关于两种平台推流(接入)的异同
1、监控平台虽然不需要主动推流,但实际情况更加扑朔迷离,各种私有协议花样百出应接不暇,没有标准接入协议真的很难搞。
谁知道明年会不会出个新的协议或者厂商设备换个型号私有头也跟着变,但是接入这块有个好处,能养很多NB程序猿,技术难度高,门槛高(最基础的你得熟悉rtsp,sip,ts,rtmp,flv,mpeg4/h264,g711,aac这些吧,至于用什么编程语言这都是后话了)。
2、直播平台一般可以移动端推流和pc端推流,用浏览器推流不知道脑袋是怎么想的,还是暂时远离webrtc这个坑吧,再过五六年或许可以。

 

 

 

Viewing all 532 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>