客户行为平台使用指南0415版

1.平台简介

客户行为数字化平台(Gamma Insight Platform)是一个面向开发者的平台,使用的开发语言是C++。该平台旨在帮助开发者解决AI算法落地门槛高,成本高,实现周期长,高度定制化造成的重复开发等问题,建立健全一个有用、高效、支持完备的平台。在我们的平台上集成和开放了很多计算机视觉方向的AI能力,在网页上就可以轻松地实现对这些能力的调用,再加上平台本身提供的软件工具,可以很方便地打造面向自己的业务、解决自己需求的软件哦。

  1. 利用此平台网页开发你需要:
  • c++
  • 能联网的浏览器(xjk-dev or pinganNet)
  1. 利用此平台部署在本地你需要:
  • 有GPU的主机
  • RTSP/USB摄像头(非必须,可以有待处理的视频、图片文件等)
  • 主机安装Ubuntu 16.04
  1. 利用此平台进行后端开发(to 平台维护者),除上述以外,你需要:
  • 代码网址
  • 安装依赖

2.平台初印象

step1. 登录地址:http://192.168.137.83:8081(内网,xjk-dev)
或者http://192.168.35.192:8081(内网,pinganNet)
因网络速度与稳定性原因,目前阶段会在网络之间切换,两个都试试。

登录账户密码为:admin:123456

登录界面

step2. 点击新建技能,直接感受保存、编译、运行、查看预览效果


新建技能
按顺序点击保存、编译、运行、查看预览效果

可以看到,当前这个功能的结果,不仅用人脸框追踪人脸的动向,而且识别出来这张人脸是谁。


预览效果1
预览效果2

为了体验平台上目前已经支持的功能,请查看 网址https://gitlab.com/GammaLab_AI/freeforward_build/tree/master/etc
下的文件并将其内容替换网页端已有的json文件即可。目前可运行的包括:
1) ff.gpu.face.json ——人脸检测
2)ff.facefeature.json ——人脸识别
3)ff.objectDetect.json ——物体识别
4)ff.person.interest.json ——人脸识别 + 微表情识别
5)ff.face.count.json ——人脸检测 + tracking工具
6)ff.attention.identify.json ——人脸识别 + 头部姿态

step 3. 当你发现有一个功能适用场景与你想要的刚好匹配,想要下载这个执行体并部署于本地,处理你本地的视频文件、图片或者视频流的话,你需要:
第一步:布置好本地环境(详见Chap 4-1)
第二步:下载这个执行体(下载地址:方法详见Chap 4-2)
第三步:解压并修改配置文件 (配置文件详细解析见Chap 4-3)
第四步:用终端打开当前路径,执行指令:./freeforward -c freeforward.json(文件名可自定义)

比较理想的结果如下图所示,能够处理你本地的视频流输入啦


本地运行人脸检测技能

至此为止,你已经获得一个完整的能运行在你本地的技能啦!

(或许并不想要下载到本地,而是直接在线上进行分析拿到日志、表格等统计信息即可呢?使用者上传视频,获取结果,我们获得视频,涉及付费模式)

3.平台初体验

现在你已经看到平台上能做到的功能啦,利用这些功能的组合或者部分中间结果可以实现你想要的需求,那么可以开始动手打造你自己的一个技能啦!下面就以“数人头”这个需求为例展示给各位如何实现吧。

(以后打开每一个算法模块都要呈现完整的算法的类名,算法技能对外提供的技能和接口,可以查看但不能修改源文件,修改源文件需要权限设置)

1)首先查看能用的算法和工具吧。数人头的需求本身并没有针对“这个人是谁”提出需求,也就是说我们只要用到“人脸检测”模型,而不需要用到“人脸识别”模型。“人脸检测”推荐使用已有的封装类:CFaceWrapperSkillGpu。而除此之外,如果你有相关的工程经验,那么就会知道,要对同一个人脸进行不重复的计数,并能够排除“掉帧”或“个别帧检测人脸失败”的情况,那么“人脸追踪”功能就是必不可少的。平台上已经提供了IOU_Tracking模块,类名为IOU_Tracking,能帮助我们的技能实现有效的平滑效果;

(头文件批量包含的问题需要修改)

2)知道了平台能提供的功能,就要定制我们自己的需求啦。假设我们希望这个技能的需求如下:

需求明细项 呈现方式/功能 需实现方式
处理材料 视频文件 通过json文件配置
视觉要求 在视频处理中实时显示已经出现的人头数目 定义user_metadata内容
算法模型 实现对图像的“人脸检测”功能 利用类:CFaceWrapperSkillGpu
工具类 实现人脸框追踪 利用类:IOU_Tracking

按照这个需求我们可以确定实现的逻辑:设立计数器初始值为0,当检测到人脸时,将人脸框(坐标、大小)送到IOU_Tracking模块进行判断,是不是可以认为是一个全新的人脸框。如果是,计数器加1,否则不变。

3)进行网页端开发。在我们的设计中,开发者核心需要实现的函数是infer,以及postInfer两个。大部分已经填写好,skill.h文件修改内容如下所示:

插入代码

skill.h

#ifndef CUSTOM_SKILL_H_
#define CUSTOM_SKILL_H_
#pragma once


#include <vector>
#include <string>
#include <unordered_map>

#include <opencv2/opencv.hpp>
#include <Eigen/Dense>


#include "../skill.h"
#include "./vip_skill/faceWrapper/face_wrapper_skill_gpu.h"
#include "../../utilities/faceRelated/IOU_Tracking.h"



struct FFRuntime;

class IInferer;

namespace cv {
    class Mat;
}


class CCustomSkill : public ISkill {

private:
    FFRuntime * ffrt_;
    std::vector<std::string> user_metadata_;
    std::string interface_path_;
    bool    isLocalMode_;

public:
    explicit CCustomSkill(const Json::Value & params, FFRuntime * ffrt);
    ~CCustomSkill();
public:
    virtual int PreInfer(int pipeid, const std::vector<int>& camids, const std::vector<int> & frameids, const std::vector<cv::Mat> & imgs);
    virtual int Infer(int pipeid, std::vector<IInferer *> & inferers, const std::vector<int>& camids, const std::vector<int> & frameids, const std::vector<cv::Mat> & imgs);
    virtual int PostInfer(int pipeid, const std::vector<int>& camids, const std::vector<int> & frameids, const std::vector<cv::Mat> & imgs);

public:
    virtual int NotifiedEnded(int pipeid, const std::vector<int>& camids, bool ended=false);


//==================below is new added=============================================
private:
        CFaceWrapperSkillGpu* face_detector_ = NULL;
        int face_count_ ;
        IOU_Tracking* iou_tracker_ = NULL;

//==================above is new added=============================================


};

#endif

由代码中可见,实际新增内容是在末尾处定义的三个private变量,CFaceWrapperSkillGpu型指针用以实现人脸检测,而face_count_用以实现持续计数充当计数器,IOU_Tracking用以实现人脸框追踪。

skill.cpp文件修改内容则如下所示:

#pragma GCC diagnostic ignored "-Wsign-compare"

#include <vector>
#include <string>

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

#include "../../runtime/inferer/inferer.h"
#include "../../runtime/view/view.h"
#include "../../runtime/db/db.h"
#include "../../runtime/ffrt.h"
#include "../../runtime/view/guiview/guiview.h"
#include <json/json.h>
#include <sys/stat.h>


#include "custom_skill.h"
#include "./vip_skill/faceWrapper/face_wrapper_utilities.h"

using namespace FACEWRAPPER2;


CCustomSkill::CCustomSkill(const Json::Value & params, FFRuntime * ffrt)
    : ffrt_(ffrt)
{
    std::string file_path = params["user_path"].asString();

    interface_path_ = params["interface_data_path"].asString();
    isLocalMode_ = (params["isLocalMode"].asInt()==1)?true:false;

    int ret = mkdir(interface_path_.c_str(), 0777);
    if(ret){
        std::cout << "[face_count_skill] CCustomSkill: mkdir fail" << ret <<std::endl;
    }
//==================below is new added=============================================

    face_detector_ = new CFaceWrapperSkillGpu(params, ffrt);
    iou_tracker_ = new IOU_Tracking();
    face_count_ = 0;
//==================above is new added=============================================

}

CCustomSkill::~CCustomSkill()
{

}

int CCustomSkill::PreInfer(int pipeid, const std::vector<int>& camids, const std::vector<int> & frameids, const std::vector<cv::Mat> & imgs)
{
    return 0;
}

int CCustomSkill::Infer(int pipeid, std::vector<IInferer *> & inferers, const std::vector<int>& camids, const std::vector<int> & frameids, const std::vector<cv::Mat> & imgs)
{
//==================below is new added=============================================

    if(!face_detector_ && isLocalMode_)
    {
        std::cout<<"face_detector_ has not be constructed! Attention!"<<std::endl;
        return -1;
    }
    if(!iou_tracker_ && isLocalMode_)
    {
        std::cout<<"iou_tracker_ has not be constructed! Attention!"<<std::endl;
        return -1;
    }
    int ret = face_detector_->Infer(pipeid, inferers, camids, frameids, {imgs});


    if(ret)
    {
        return -2;
    }
    std::vector < cv :: Rect > faceBoxes;
    face_detector_->getFaceBoxes( faceBoxes);

    std::vector < std :: vector < cv :: Point >> landmarks;
    face_detector_->getLandmarks(landmarks);

    std::vector<std::string> box_ids;
    std::vector<bool> newid_state;
    int ret2 = iou_tracker_->tracking(faceBoxes, box_ids, newid_state);
    if(ret2 == false){
        std::cout << "[face_count_skill] [infer] iou_tracker_ : mkdir fail" << ret <<std::endl;
    }

    int i = 0;
    for (std::vector<std::string> :: iterator it = box_ids.begin(); it != box_ids.end(); it++)
    {
        if (newid_state[i]) {
             face_count_ ++;
        }

    }
//==================above is new added=============================================

    return 0;
}

int CCustomSkill::PostInfer(int pipeid, const std::vector<int>& camids, const std::vector<int> & frameids, const std::vector<cv::Mat> & imgs)
{
    cv::Mat img_out;
    if(ffrt_->view){
        CGuiView* viewer = (CGuiView*)ffrt_->view;
        user_metadata_.clear();
//==================below is new added=============================================
        user_metadata_.push_back("ENTER NUM:"+std::to_string(face_count_));
        viewer->displayG(imgs.at(0),user_metadata_,img_out, isLocalMode_);
//==================above is new added=============================================

        if(!isLocalMode_){
            std::string file_name = interface_path_ +"/" + std::to_string(frameids.at(0)) + ".jpg";
            cv::imwrite(file_name, img_out);
        }
    }
    return 0;
}

int CCustomSkill::NotifiedEnded(int pipeid, const std::vector<int>& camids, bool ended)
{
    return 0;
}

由文件中修改可见,实际需要修改三个函数:构造函数、Infer也就是调用前向的函数、以及后处理函数PostInfer。

修改json文件(配置文件详细解析见Chap 4-3):

{ 
  "skill":{
    "type": "custom",
    "param": {
        "user_path":"/media/zhangjing/data/Project/freeforward_build/models/face_one2N_model/",
        "interface_data_path":"shared",
        "isLocalMode": 1
    }
  },
  "pipes": [
    {
      "inputtest": {
        "type": "usb",
        "param": {
          "mrl": "v4l2:///dev/video0",
          "cords": [ 1.0, 1.0, 1.0, 2.0, 3.0 ]
        }
      },
      "input": {
        "type": "rtsp",
        "param": {
          "mrl": "file:///var/gammalab/freeforward_build/test_video/count.mp4",
          "cords": [ 1.0, 1.0, 1.0, 2.0, 3.0 ]
        }
      },
      "inferers": [
        {
          "type": "caffewrapper",
          "param": {
            "cpu": false,
            "id" : "pNet",
            "weight": "./models/face_one2N_model/det1.caffemodel",
            "net": "./models/face_one2N_model/det1-memory_gpu.prototxt"
          }
        },
        {
          "type": "caffewrapper",
          "param": {
            "cpu": false,
            "id" : "RNet",
            "weight": "./models/face_one2N_model/det2.caffemodel",
            "net": "./models/face_one2N_model/det2-gpu.prototxt"
          }
        },
        {
          "type": "caffewrapper",
          "param": {
            "cpu": false,
            "id" : "ONet",
            "weight": "./models/face_one2N_model/det3.caffemodel",
            "net": "./models/face_one2N_model/det3-gpu.prototxt"
          }
        }
      ],
      "DUMMY": { }
    }
  ],
  "view": {
        "type": "guiview",
        "param": { }
  },
  "db": {
    "type": "redis",
    "param": { }
  },
  "logger": {
    "type": "consolelogger",
    "param": { }
  },
  "DUMMY": { }
}

看起来很繁琐但其实其中满满的都是套路哦,先直接使用,下一章再详解。

4)好啦,核心的部分已经搞定啦,快来看看我们第一个技能的能不能运行,以及效果如何吧。一路点击保存、编译、运行、查看预览效果。
须知(for now):
a.编译以后得到确切的编译成功信息后再点运行这一步。这个信息可以点击查看编译信息得到


查看编译信息

b.运行以后大致等2min左右的时间后再查看预览效果,否则结果未完全生成;


运行结果1

运行结果2

看,按照我们的预期运行起来了~

5)然后就可以下载到本地实际试试啦。假设作为开发者的你已经布置好本地环境了(详见Chap 4-1),那么剩下的操作步骤与Chap2的step3没有什么区别啦。我们拿本地的一个视频测试一下(示例文件地址:https://gitlab.com/GammaLab_AI/freeforward/tree/master/shared/testvideos ,也可以自己准备示例视频)。

本地运行结果

注意左上角有对人数实现计数。
也就是说,我们部署在本地,实现了在商店里计算客流量的的功能。是不是可以由此扩展到更多的业务场景呢?

4.平台配置导读

以下内容是你想深入了解平台、更好地利用平台开发所需的information:

1)布置能运行这个执行体的本地环境
a. 必不可少的是Ubuntu16.04系统安装好,网上有很多参考教程(Lab内开发有统一的版本);
b. 你需要安装Linux的基本依赖,这些依赖如下:

- PACKAGES="vlc libvlc-dev liblog4cxx-dev libhiredis-dev redis-server python3-zmq qtbase5-dev qttools5-dev-tools 
libdbus-1-dev libharfbuzz-dev libgflags-dev libgoogle-glog-dev libblas-dev libhdf5-serial-dev hdf5-tools 
libleveldb-dev liblmdb-dev libboost-all-dev libopenblas-dev libsnappy-dev libeigen3-dev openexr build-essential
 cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev python-dev python-numpy libtbb2 
libtbb-dev libjpeg-dev libpng-dev libtiff-dev libjasper-dev libdc1394-22-dev protobuf-compiler libprotobuf-dev 
curl libssl-dev"

以上依赖的安装方法:

  - sudo apt-get install -q -y XXX;
e.g. sudo apt-get install -q -y libvlc_dev;

建议以上安装包以几个一组安装,不宜一条指令全部安装,否则有个别失败的安装项容易被忽略掉。

c.安装OpenCV3.1

curl -kLO https://github.com/Itseez/opencv/archive/3.1.0.zip && unzip 3.1.0.zip  && cd opencv-3.1.0 
&& mkdir -p build && cd build && cmake -D CMAKE_BUILD_TYPE=Release .. && make && sudo make install 

在安装OpenCV3.1的过程中可能会遇到下载ippicv_linux_20151201.taz失败的情况,参考解决办法见:
https://blog.csdn.net/shangpapa3/article/details/79481883

或者直接使用网址:

c. 还需要安装nvidia的cuda工具包及其类库cudnn,版本已放在XXX网址 在安装的时候注意有显卡驱动的选择可能会引起安装失败,需要使用指令:apt-get install nvidia-384,同时在安装cuda的时候当问到是否安装适配驱动时选择否。剩余的安装步骤可参考:
https://blog.csdn.net/xunan003/article/details/82145596

2)下载执行体

(未来会在技能开发页面上直接呈现下载地址,当前需要找到Jenkins对应地址,将其下载下来)

a. 打开网页地址:192.168.137.70:8000(under xjk-dev)或者192.168.30.171:8000(under pinganNet).登录账户密码:gammalab:gammalab
b. 按照编号找到freeforward_build任务,按照触发时间点可以找到对应的任务编号,等待任务顺利完成之后即可以得到生成结果的下载链接。

3)配置文件解析
下载后解压,得到的文件内容如下所示:


本地解压并运行

修改配置文件freeforward.json文件,该文件分段解析如下:

(user_path的使用之后会是个问题)
skill一项是可以由用户在编写skill.cpp中可以直接获取的一项,默认通过网页开发的用户skill[type]都是custom。
  "skill":{
    "type": "custom",
    "param": {//用户自定义。但不建议修改,除非在代码中需要用到user_path来读取文件,适用于本地调试
        "user_path":"/media/zhangjing/data/Project/freeforward_build/models/face_one2N_model/",
        "interface_data_path":"shared",
        "isLocalMode": 1//1-本地模式,适用于本地调试;0-服务器模式,网页端开发使用
    }
  },
  "pipes": [先把pipe内部结构简化以明确pipes的范畴,一个pipe对应一项输入(一个视频流或者一个视频文件),pipes本身是数组
    {
      "input": {此项定义输入类型
        ...
       },
      "inferers": [此项列出该技能对应本输入需要依次调用的算法模型,也是以数组的方式
        {
          ...
        },
        {
          ...
        },
      ],
      "DUMMY": { }//暂时无用
    }
  ],

以下分析pipes中的单项:

以下各个类型,在本地调试时都需要适当修改mrl
//USB型input
"input": {
        "type": "usb",
        "param": {
          "mrl": "v4l2:///dev/video0",
          "cords": [ 1.0, 1.0, 1.0, 2.0, 3.0 ]
        }
      },
//RTSP型input,亦可以处理视频文件
      "input": {
        "type": "rtsp",
        "param": {
          "mrl": "file:///var/gammalab/freeforward_build/test_video/count.mp4",
          "cords": [ 1.0, 1.0, 1.0, 2.0, 3.0 ]
        }
      },
//图片型input,目前图片命名规则为自然数,如1.jpg,2.jpg等
 "input": {
        "type": "imagefiles",
        "param": {
          "mrl": "/home/yons/workingCopys/freeforward_build/user_data/pics",
          "cords": [ 1.0, 1.0, 1.0, 2.0, 3.0 ]
        }
      },
注意:以上是不同的示例,在有效的json文件中只能保留一种input

以下是inferers,也就是控制调用模型的配置项。模型的具体地址参见(https://gitlab.com/GammaLab_AI/freeforward/tree/master/models)

      "inferers": [//这里的顺序对应代码中的调用顺序,需要一一对应
        {
          "type": "caffewrapper",//推荐使用,对应caffe-binding模式
          "param": {
            "cpu": false,//false则对应使用GPU,true则对应CPU
            "id" : "pNet",
            "weight": "./models/face_one2N_model/det1.caffemodel",
            "net": "./models/face_one2N_model/det1-memory_gpu.prototxt"
          }
        },
        {
          "type": "caffewrapper",
          "param": {
            "cpu": false,
            "id" : "RNet",
            "weight": "./models/face_one2N_model/det2.caffemodel",
            "net": "./models/face_one2N_model/det2-gpu.prototxt"
          }
        },
        {
          "type": "caffewrapper",
          "param": {
            "cpu": false,
            "id" : "ONet",
            "weight": "./models/face_one2N_model/det3.caffemodel",
            "net": "./models/face_one2N_model/det3-gpu.prototxt"
          }
        }
      ],
本例是人脸检测所使用到的模型。

5.平台后台开发

如果你想要深入本平台的后台开发和维护工作,或者你是一位希望将此平台打造成更具适配性、兼容性的开发者,请阅读此章节。如果你想做的只是利用平台实现你的需求,本章相当于试卷上的附加题,做不做随你。

平台整个后端的工程地址:https://gitlab.com/GammaLab_AI/freeforward_build

1)工程的base类以及继承关系


base类以及继承关系

写不动了。。。
2)技能的概念
继承自skill,有三个需要实现的函数。。。。
3)平台上当前支持的两种infer方式。caffe-binding以及原生caffe。分别有示例。
4)平台AI能力详情列表
5)平台工具类开发建议
faceCommonTools为例
6)平台开发调试指南
step 1.
step 2.
step 3.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,474评论 1 45
  • 1.git的安装 1.1 在Windows上安装Git msysgit是Windows版的Git,从https:/...
    落魂灬阅读 12,661评论 4 54
  • 第一篇先写给自己 忘记了上一次打开简书是什么时候的事,一直想写,却害怕开始,怕自己坚持不下去,怕自己文笔幼稚,怕自...
    阿杜故事阅读 265评论 0 1
  • 编程向导:4.8图形 一、画布介绍 部件绘画的表现使用画布来完成。你可以将它看作一个无限制的画板,或者一个绘画指令...
    gthank阅读 817评论 0 0
  • 考试周的前一周,事情不多,但是很耗时,我知道自己到那个点肯定是会把他做完的,所以是急又不急的状态。感觉要看的东西很...
    倾国是故国曦阅读 180评论 0 0