Deep Coding


  • 关于

  • 标签

  • 日志

  • 首页

图像处理中最常见的三种图像读取方式

发表于 2018-06-07 |

背景

最近计算图像残差时,尝试了用三种方式读取图像,很多时候读取的图像往往都是unit8类型,这样便导致残差的计算全为正数的情况,需要通过numpy类型转换函数进行转换

opencv

type查看python中元素的类型(list、dict、string、float、int)
dtype查看numpy ndrray的数据类型(float、int)
numpy中使用astype,及直接指定dtype = 'float32'进行转换,其中dtype可以省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import cv2
import numpy as np
s1 = cv2.imread('1.png')
print (type(s1)) # <type 'numpy.ndarray'>
print (s1.dtype) # uint8
# 通过下面的函数进行数据类型转换,再进行残差计算
s1 = s1.astype(int16)
s1 = s1.astype(float32)
s1 = np.array(s1, 'int16')
s1 = np.array(s1, dtype='int16')
# 保存图片前先Scale到[0, 255]之间
s1 = np.maximum(ss, 0) # < 0 = 0
s1 = np.minimum(ss, 255) # > 255 = 255
cv2.imwrite("Recon.png", s1)

PIL

采用PIL模块的Image.fromarray(image).save方式存取的是无损图像,一般的图像差值处理顺序是:float32, clip(0,1)*255, round, cast uin8

1
2
3
4
5
6
7
8
9
import numpy as np
from PIL import Image
import scipy.misc
s1 = Image.open('1.png')
image_1 = np.array(s1)
print (image_1.dtype) # uint8
image1 = np.array(img1, dtype='float32')
print (image_1.dtype) # float32
Image.fromarray(image).save(test_set_dir + str(i)+'.png')

Tensorflow

用tensorflow直接保存图像的情况使用较少,可以预先产生一个tf.gfile.FastGFile文件,接着将图片信息写入即可,注意这里又多出了两种类型转换函数:tf.image.convert_image_dtype,tf.cast

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
import tensorflow as tf

img_name = ["1.jpg"]
filename_queue = tf.train.string_input_producer(img_name)
img_reader = tf.WholeFileReader()
_,image_jpg = img_reader.read(filename_queue)

image_decode_jpeg = tf.image.decode_jpeg(image_jpg)
# 两种类型转换函数
image_decode_jpeg = tf.image.convert_image_dtype(image_decode_jpeg, dtype=tf.float32)
# image_decode_jpeg = tf.cast(image_decode_jpeg, tf.float32)

sess = tf.Session()
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)

image_flip_up_down = tf.image.flip_up_down(image_decode_jpeg)
image_flip_up_down = tf.image.convert_image_dtype(image_flip_up_down, dtype=tf.uint8)
image_flip_up_down = tf.image.encode_jpeg(image_flip_up_down)

img_up_down = sess.run(image_flip_up_down)
hd = tf.gfile.FastGFile("ud.png", "w")
hd.write(img_up_down)
hd.close()

coord.request_stop()
sess.close()
print("test end!")

Ref:

tensorflow如何保存图像

Python 绘制简单的折线图

发表于 2018-06-05 |
import matplotlib.pyplot as plt  
from pylab import *
"""
绘制简单的折线图
"""
x1 = [1, 2, 5]  
y1 = [28.40,30.61,33.66]

x2 = [1.75, 2.7, 5.9]  
y2 = [28.53,30.28,32.45] 

x3 = [1.9, 2.98, 5.1]  
y3 = [29.49,31.41,33.60]  

plt.title('H264(b) vs TTG(r) vs BPG(g)')  
plt.xlabel('Mbps')  
plt.ylabel('Average psnr')  

plt.plot(x1, y1,'b', label='H264')  
plt.plot(x2, y2,'r',label='TNG')  
plt.plot(x3, y3,'g',label='BPG')  

# 设置横轴的上下限
xlim(1.0,7.0)

# 设置横轴记号
xticks(np.linspace(1,7,6,endpoint=True))

# 设置纵轴的上下限
ylim(28.0,34.0)

# 设置纵轴记号
yticks(np.linspace(28,34,4,endpoint=True))

plt.grid()  
plt.show() 

tensorflow入门示例

发表于 2018-05-30 |

逻辑回归分类示例

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
import tensorflow as tf
"""
Mnist by tensorflow
"""
# 通过Tensorflow自带模块,加载Mnist数据集
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
# Tensorflow依赖于高效的C++后端来进行计算,与后端的连接叫做session,一般是先构建图,然后在session中启动它
# 更方便的是InteractiveSession类,可以在运行图的时候,动态构建图
sess = tf.InteractiveSession()
# 占位符
# 我们通过为输入图像和目标输出类别创建节点,来开始构建计算图。
x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])
# 变量 w 为一个[784.10]的矩阵,b是一个10维向量
w = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
# 变量初始化
sess.run(tf.global_variables_initializer())
# 实现softmax回归模型
y = tf.nn.softmax(tf.matmul(x,w)+b)
# loss:整个Mini batch的交叉熵
cross_entropy = -tf.reduce_sum(y_*tf.log(y))
# BP算法
# 返回的train_step对象,运行时会使用梯度下降来更新参数,整个模型的训练通过反复运行train_step来进行
train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
# 开始反复训练
for i in range(1000):
batch = mnist.train.next_batch(50)
# feed_dict 用数据替换张量x, y_
sess.run(train_step, feed_dict={x:batch[0], y_:batch[1]})

# 模型评估
prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
# 将上面返回的bool数组转换为float,用以计算准确率
accuracy = tf.reduce_mean(tf.cast(prediction, "float"))
Acc = sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels})
print ("Accuracy: ", Acc)

CNN分类示例

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
sess = tf.InteractiveSession()
x = tf.placeholder("float", shape=[None, 784])
y_ = tf.placeholder("float", shape=[None, 10])
# CNN网络训练时需要随机初始化权重
# tf.truncated_normal(shape, mean, stddev) shape表示生成张量的维度,mean是均值,stddev是标准差
# 截断正态分布函数,产生正态分布的值如果与均值的差值大于两倍的标准差,那就重新生成
def weight_variable(shape):
initial = tf.truncated_normal(shape, stddev=0.1)
return tf.Variable(initial)
def bias_variable(shape):
initial = tf.constant(0.1, shape=shape)
return tf.Variable(initial) # 用0.1赋值的常量
def conv2d(x, W):
return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')
def max_pool_2x2(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

# patch 大小5*5
W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

x_image = tf.reshape(x, [-1,28,28,1]) # reshape_1,batch*W*H*C

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64]) # reshape_2 变为1维向量
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob) # 添加一个占位符,在train、test中灵活改变keep_prob比例

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

# 模型其他参数
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
sess.run(tf.global_variables_initializer())
for i in range(20000):
batch = mnist.train.next_batch(50)
if i % 100 == 0:
train_accuracy = sess.run(accuracy, feed_dict={x:batch[0], y_: batch[1], keep_prob: 1.0})
print ("step %d, training accuracy %g" % (i, train_accuracy))
train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5}) # 循环往复执行优化计算
Acc = sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0})
# 也可使用Tensor.eval() 和 Operation.run() 方法代替 Session.run()
# print (accuracy.run(feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
print ("test accuracy", Acc)

Tensorflow 数据读取机制

发表于 2018-05-28 |

Ref

tensorflow载入数据的三种方式
Tensorflow从文件读取数据

图像质量评估算法PSNR、MS-SSIM

发表于 2018-05-22 |

PSNR计算

psnr是“Peak Signal to Noise Ratio”的缩写,即峰值信噪比,是一种评价图像的客观标准,衡量原图像与被处理图像之间的均方误差,单位db。两个图像之间PSNR值越大,则越相似。普遍基准为30dB,30dB以下的图像劣化较为明显。公式如下:$$PSNR=10\times log_{10}(\frac{(2^{n}-1)^{2}}{MSE})$$ n为图像位数(注意即使为RGB三通道图像:n==8),MSE是原图像与测试图像所有像素之间的误差平方和,再取平均(如下代码所示)

1
2
3
4
5
6
7
8
9
10
class PsnrEstimator(object):
"""psnr评估器
"""
def func(self):
se = tf.square(self.tf_origin_image - self.tf_recon_image)
mse = tf.reduce_mean(se) # 每个通道分别计算MSE取平均
psnr = tf.cond(tf.equal(mse, 0),
lambda: tf.constant(100, dtype=tf.float32),
lambda: 10 * (tf.log(255 * 255 / mse) / np.log(10))) # 换底公式
return se, psnr

另外需要注意的一点是,假如计算图像残差的PSNR,因为本身残差图像像素值就很小,所以即使训练的图像与原始残差PSNR值较高,但差距依然可能很大。所以对于残差图像的PSNR,要求要更加严格。

深度神经网络架构发展史

发表于 2018-05-17 |

Ref

深度概览卷积神经网络全景图
一文带你了解深度神经网络架构发展史

JPEG图像压缩原理解析

发表于 2018-05-14 |

背景

JPEG是一种有损压缩技术,在保证视觉效果差别不大的情况下,压缩比可以达到几十倍。它的原理是去除图像中一些不重要的部分,从而用更小的体积保存。一个形象的例子就是“四舍五入”,比如“2.1000000001”,保存为“2.1”,就去掉了“0.0000001”,保留了重要部分做原数字的近似。

图像分割

首先将RBG图像转换为YUV(亮度-色差)图像,这是因为人的视觉系统对亮度变化最为敏感,YUV图像可以把亮度channel单独剥离进行处理,更适合图像压缩场景。如图是一幅彩色图像的YUV分量,从中可以看到Y通道携带了图像的大部分细节信息图片名称 接着将图像分割成$8\ast 8$的小块,用于后续$DCT$变换的单独处理。

DCT离散余弦变换

DCT可以将图像以频域的形式进行表达,经过DCT变换,图像左上角会集中低频部分的能量,右下角集中高频,数值越大,代表此处的能量越多。下图是某前辈对Lena图的第一个$8\ast 8$小块进行DCT变换的结果。图片名称

量化

到目前为止依然是无损的,由于人眼对高频不敏感的特性,我们试图过滤这些右下角的高频分量,此时就要用到量化。说白了就是分别scale,把左上角的除一个较小值,右下角的除一个较大值,接着对结果进行取整,这样就会导致右下脚大部分变为0,非常有利于后面的压缩。图片名称 但量化时候并不是随便scale,经过长期的经验,人们得到了上面两张量化表,对应位置相除即可(在量化表上乘一个系数,可以得到不同的压缩比)。量化后的结果如下,右下角已大部分为0。
图片名称 接着将上述矩阵按Z形编码,得到一个一维数组:
Eg:35,7,0,0,0,-6,-2,0,0,-9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,…,0,目的是尽可能使0分布在一起。接着主要对非0部分进行编码:采用JPEG提供的标准码表,及霍夫曼编码(无损),得到最终的压缩数据。

小结

JPEG编码流程:图像色彩空间变换——DCT——量化(有损)——霍夫曼编码
解码流程:霍夫曼编码恢复——反量化——IDCT——重建图

Ref

JPEG哈夫曼编码数组
JPEG算法解密

两种常用的图像检索算法

发表于 2018-05-07 |

背景知识

Mat类型数据结构

一个图像的通道数是N,就表明每个像素点处有N个数,一个a×b的N通道图像,其图像矩阵实际上是b行N×a列的数字矩阵。
p = img.ptr<uchar>(i); 图像第i行的头指针,通过这个指针结合列的位置(就是你代码中的j)可以很轻松操作图像改行的每一列,即p[j]
samples.at<Vec3f>(k,0)[0] = float(p[j*3]); at <类型> (行,列) [通道(如果有通道的话)],获取某点像素值

kmeans

总体目标是最小化聚类中心与每个样本的欧式距离,先随机初始化K个聚类中心,根据欧式距离进行划分,更新聚类中心,不断重复,直到达到指定的聚类次数或者收敛为止。缺点是需要指定K类,以及初始点对划分结果影响很大

kNN

有监督的分类算法,K个最相似(欧式距离)的样本中的大多数属于某个类别,则该样本也属于该类

欧式距离

指的是两个向量的距离相似度,欧式距离越小代表向量越趋于相似(二、三维空间中就是点的实际距离),公式如下$$\rho = \sqrt{\sum_{i=1}^{n}(x_{i}-y_{i})^{2}}$$

余弦距离

指的是两个向量的方向相似度,余弦值越大(归一化后点乘),代表向量越相似。

图像相似度匹配算法

BOF(Bag of Features)算法

  • 视觉单词提取:提取所有图片的ORB特征并concat到一起[M, N, D]
  • 视觉词典构建:采用K-Means算法,聚成K个类,每个聚类有一个类心,那么这K个类心就相当于词袋中的K个基本词汇。
  • 图像表示:统计每张图片在每个聚类中的特征个数,这样每个图片就可以由一个分布直方图表示,或者说用一个K维的向量表示,向量第i维的值为该图片在第i个聚类中的特征个数。那么每个图像都可以由基本词汇表示。
  • 图像检索:对于一个待检索的图像,我们先提取该图像的图像特征,并产生该图像的分布直方图(K维向量)。然后进行图像向量的余弦相似度度量便可以了

VALD算法:

  • ORB对每张图像提取特征[N, D]
  • 聚类[K,D]
  • 以每个center为中心,进行残差计算并累加(每个N和其对应的center(label))[K, D]
  • 将矩阵拉成一个[K, D]的长向量,对该向量的每一个分量进行幂律归一化(需保持符号不变),公式如下:$$v_{j} := \left | v_{j} \right |^{\alpha } \times sign(v_{j}), j = 1,2, … D$$幂律归一化的目的是主要为了减少某些特征出现次数特别多带来的影响,这是因为出现次数特别多时聚类中心就在它附近,相应的残差就很小,这样便可以抑制数值大的数同时提升数值小的数
  • 对每个分量进行L2范数归一化 normalize(Vlad,Vlad,-1.0,1.0,NORM_L2)
  • 采用PCA进行降维,得到该图像的VALD特征向量,之后进行图像向量的余弦相似度度量便可以了

Git相关操作

发表于 2018-05-04 |

背景

上传本地项目到 github或公司私有服务器中,便于代码的存储、协同操作,步骤如下:

1
2
3
4
ssh-keygen -t rsa -C "wuxiangji@tucodec.com" 
# 上述命令会在用户主目录的.ssh目录下,生成id_rsa(私钥)和id_rsa.pub(公钥)
# 登陆Git,将公钥添加到“Account settings”,“SSH Keys”即可
# Git的ssh key本质上就是建立了本机与服务器的连接,而且免去了以后输入密码的烦恼

git为不同的项目设置不同的用户名

1
2
3
4
5
# cd 项目跟目录
git init # 建立本地git仓库
git config user.name "gitlab's Name"
git config user.email "gitlab@xx.com"
git config --list # 查看当前配置, 在当前项目下面查看的配置是全局配置+当前项目的配置, 使用的时候会优先使用当前项目的配置

配置多账户ssh key

在 ~/.ssh 目录下新建一个config文件

1
2
3
4
5
6
7
8
9
10
11
Host 192.28.1.81
HostName 192.28.1.81
Port 2222
User Xiangji_WU
IdentityFile ~/.ssh/id_rsa_xxx

Host 192.28.1.83
HostName 192.28.1.83
Port 2222
User Xiangji_WU
IdentityFile ~/.ssh/id_rsa_yyy

使用命令行进行上传

1
2
3
4
5
6
7
# cd 项目跟目录
git init # 建立本地git仓库
git add .
git commit -m "first commit"
git remote add origin git@github.com:Wxjwjj/Facial-Landmark.git # 关联某个远程库
git pull origin master # 要取回origin主机的分支,与本地的master分支合并
git push -u origin master # 第一次推送master分支的所有内容

使用命令行进行代码更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git status 					# 查看当前的git仓库状态
git diff readme.txt # 查看修改后的readme.txt和之前的区别
git add readme.txt # 文件添加到暂存区 注意每次修改,如果不add到暂存区,那就不会加入到commit中
git commit -m "更新说明" # 暂存区的内容提交到当前master分支
git log --pretty=oneline # 查看一共提交了多少版本
git reflog # 查看命令历史
git reset --hard head^
# HEAD指向的版本就是当前版本,当要在多个版本的历史之间穿梭,使用命令 git reset --hard commit_id
git reset HEAD readme.txt # 已经添加到暂存区的修改
git checkout -- readme.txt # 进撤销工作区内容的修改
git checkout -- readme.txt # rm指令误删的文件可以从版本库中进行恢复
git rm readme.txt # 从版本库中彻底删除,并且要进行commit
git commit -m "remove test.txt"
git pull # 当前分支自动与唯一一个追踪分支进行合并
git push origin master

实时人脸68关键点检测 for Mac/iOS

发表于 2018-05-04 |

背景

前段时间的人脸68关键点检测项目,需要移植到手机端,而且需要达到实时性,所以对算法进行了改进。大致的解决方案是,由于人脸检测耗时较大,所以搭配追踪、模板匹配、ROI区域内检测算法。用一幅图说明如下所示:
图片名称

Mac端需要先配置dlib依赖

直接使用brew安装下面两个依赖

1
2
brew install openblas
brew install opencv

X11是执行Unix程序的图形窗口环境。Mac OS X本身的程序是Aqua界面的,但是为了能够兼容unix和linux移植过来的程序(Mac OS X由BSD-UNIX修改而来),比如MatLab,就需要x11窗口环境。
 
运行dlib需要X11,但Mac目前没有自带X11,需要重新下载安装,下载地址
下载后直接安装,默认安装目录为/opt/X11,需要在/usr/loca/opt目录下创建软连接(最好使用绝对地址),创建命令如下,创建后重启Mac。

1
2
cd /usr/local/opt
ln -s /opt/X11 X11

real_dlib_muban_roi.cpp

功能:实时单人脸区域检测(从摄像头读取),及人脸68个关键点位置检测
实时帧率:23 fps
视频大小:320$\ast$240 标清
硬件平台:Intel(R) Core(TM) i5 @ 2.70GHz(Mac)
算法描述:
1、先进行Face Detect得到第一帧中人脸区域;
2、将该区域放大1.2倍,取为ROI区域;
3、在ROI区域中进行模板匹配(模板为前一帧中检测的人脸)
4、假如模板匹配算法得分较低,或者bbox超出了图像边界(即人脸出界),则在整张图中重新进行Face Detect
调用指令: ./real_dlib_muban_roi

Tips:笔者也尝试过在ROI区域中不执行模板匹配,而是直接执行Face Detect,但实际人脸静止情况下,检测到的人脸位置突变极大。折腾了两三天,不断运行代码,发现位置分布居然是有规律的,就在几个固定位置跳动。
突然意识到人脸检测器不是每个像素划窗的(🤦‍♂️),而是每20/40个像素滑,这样就不能在ROI区域中完美匹配到上次的位置。所以采取了模板匹配的方法,挨个像素滑动,这样检测位置便是正确的了。

video_Seeta_Track.cpp

功能:实时单人脸区域检测(从视频读取),及人脸68个关键点位置检测
实时帧率:25 fps
视频大小:320$\ast$240 标清
硬件平台:Intel(R) Core(TM) i5 @ 2.70GHz(Mac)
算法描述:追踪代码运行超过75次,或者追踪失败的情况下,调用Face Detect函数,重新进行人脸位置检测
调用指令: ./video_Seeta_Track
检测demo附图:图片名称

real_dlib.cpp / real_seeta.cpp

功能:多人脸区域检测(从摄像头读取),及多人脸68个关键点位置检测
实时帧率:8~10 fps
视频大小:320$\ast$240 标清
硬件平台:Intel(R) Core(TM) i5 @ 2.70GHz(Mac)
算法描述:
这两个算法,仅用了face detect,而detect在人脸关键点检测项目中,占据的时间较长,所以仅用检测,无法在mac上达到实时效果。
调用指令:./real_dlib,./real_seeta.cpp

作为对比,分别对seetaface、dlib库进行了测试,发现两个face detect库耗时相当,seetaface人脸检测器,能检测到更小的人脸[40,40],人脸偏转一定角度(45度范围内),基本也能检出。而dlib的优势在于其稳定性,这个应该跟模型设计的划窗步长相关,见第二点’Tips’中的分析。

Pic_LandMark68.cpp

功能:多人脸区域检测(从图片读取),及多人脸68个关键点位置检测
算法描述:
采用Seetaface进行人脸检测,dlib进行后续的人脸关键点检测
调用指令:./build/Pic_LandMark68 1.png ./model/seeta_fd_frontal_v1.0.bin
检测demo附图:图片名称

Github代码连接

上述代码已经全部上传至github,目前仅实现了实时单人脸68关键点绘制,后面有时间话再完善多人关键点检测代码吧。

1
2
3
4
5
6
# clone该工程
cd Video_Face
mkdir build
cd build
cmake ..
make # 即可在build文件夹中生成可执行程序,调用方式如上

PS:dlib、Seetaface的相关代码我也一起打包到工程中了,如果你自己编译安装了OpenCV,用上述CMakeLists便可以。如果你没有编译OpenCV,恰巧又是Mac平台,可以使用我编译好的OpenCV动态库,就在libs文件夹,OpenCV头文件在include文件夹。
github代码链接

Ref:

Face detect模块采用山世光老师开源的seetaface
Facial Landmark模块采用了dlib库

12…5
祥吉

祥吉

CV/NLP,Studying,Coding

49 日志
10 标签
E-Mail 知乎 微博
© 2018 祥吉
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
博客全站共37.2k字
本站访客数: