深度学习AI美颜系列----基于抠图的人像特效算法

美颜算法的重点在于美颜,也就是增加颜值,颜值的广定义,可以延伸到整个人体范围,也就是说,你的颜值不单单和你的脸有关系,还跟你穿什么衣服,什么鞋子相关,基于这个定义(这个定义是本人自己的说法,没有权威性考究),今天我们基于人体抠图来做一些人像特效算法。

抠图技术很早之前就有很多论文研究,但是深度学习的出现,大大的提高了抠图的精度,从CNN到FCN/FCN+/UNet等等,论文层出不穷,比如这篇Automatic Portrait Segmentation for Image Stylization,在FCN的基础上,提出了FCN+,专门针对人像抠图,效果如下:

图a是人像原图,图b是分割的Mask,图cde是基于Mask所做的一些效果滤镜;

要了解这篇论文,首先我们需要了解FCN,用FCN做图像分割:

该图中上面部分是CNN做图像分割的网络模型,可以看到,最后是全连接层来处理的,前5层是卷积层,第6层和第7层分别是一个长度为4096的一维向量,第8层是长度为1000的一维向量,分别对应1000个类别的概率;而下图部分是FCN,它将最后的三个全连接层换成了卷积层,卷积核的大小(通道数,宽,高)分别为(4096,1,1)、(4096,1,1)、(1000,1,1),这样以来,所有层都是卷积层,因此称为全卷积网络;

FCN网络流程如下:

在这个网络中,经过5次卷积(和pooling)以后,图像的分辨率依次缩小了2,4,8,16,32倍,对于第5层的输出,是缩小32倍的小图,我们需要将其进行上采样反卷积来得到原图大小的分辨率,也就是32倍放大,这样得到的结果就是FCN-32s,由于放大32倍,所以很不精确,因此,我们对第4层和第3层依次进行了反卷积放大,以求得到更加精细的分割结果,这个就是FCN的图像分割算法流程。

与传统CNN相比FCN的的优缺点如下:

优点:

①可以接受任意大小的输入图像,而不用要求所有的训练图像和测试图像具有同样的尺寸;

②更加高效,避免了由于使用像素块而带来的重复存储和计算卷积的问题;

缺点:

①得到的结果还是不够精细。进行8倍上采样虽然比32倍的效果好了很多,但是上采样的结果还是比较模糊和平滑,对图像中的细节不敏感;

②没有充分考虑像素与像素之间的关系,也就是丢失了空间信息的考虑;

在了解了FCN之后,就容易理解FCN+了,Automatic Portrait Segmentation for Image Stylization这篇论文就是针对FCN的缺点,进行了改进,在输入的数据中添加了人脸的空间位置信息,形状信息,以求得到精确的分割结果,如下图所示:

对于位置和形状数据的生成:

位置通道:标识像素与人脸的相对位置,由于每张图片位置都不一样,我们采用归一化的x和y通道(像素的坐标),坐标以第一次检测到人脸特征点为准,并预估了匹配到的特征与人体标准姿势之间的一个单应变换T,我们将归一化的x通道定义为T(ximg),其中ximg是以人脸中心位置为0点的x坐标,同理y也是如此。这样,我们就得到了每个像素相对于人脸的位置(尺寸也有相应于人脸大小的缩放),形成了x和y通道。

形状通道:参考人像的标准形状(脸和部分上身),我们定义了一个形状通道。首先用我们的数据集计算一个对齐的平均人像mask。计算方法为:对每一对人像+mask,用上一步得到的单应变换T对mask做变换,变换到人体标准姿势,然后求均值。

W取值为0或1,当变换后在人像内的取值为1,否则为0。

然后就可以对平均mask类似地变换以与输入人像的面部特征点对齐。

论文对应的代码链接:点击打开链接

主体FCN+代码:

[python]  view plain copy
  1. from __future__ import print_function
  2. import tensorflow as tf
  3. import numpy as np
  4. import TensorflowUtils_plus as utils
  5. #import read_MITSceneParsingData as scene_parsing
  6. import datetime
  7. #import BatchDatsetReader as dataset
  8. from portrait_plus import BatchDatset, TestDataset
  9. from PIL import Image
  10. from six.moves import xrange
  11. from scipy import misc
  12. FLAGS = tf.flags.FLAGS
  13. tf.flags.DEFINE_integer("batch_size", "5", "batch size for training")
  14. tf.flags.DEFINE_string("logs_dir", "logs/", "path to logs directory")
  15. tf.flags.DEFINE_string("data_dir", "Data_zoo/MIT_SceneParsing/", "path to dataset")
  16. tf.flags.DEFINE_float("learning_rate", "1e-4", "Learning rate for Adam Optimizer")
  17. tf.flags.DEFINE_string("model_dir", "Model_zoo/", "Path to vgg model mat")
  18. tf.flags.DEFINE_bool('debug', "False", "Debug mode: True/ False")
  19. tf.flags.DEFINE_string('mode', "train", "Mode train/ test/ visualize")
  20. MODEL_URL = 'http://www.vlfeat.org/matconvnet/models/beta16/imagenet-vgg-verydeep-19.mat'
  21. MAX_ITERATION = int(1e5 + 1)
  22. NUM_OF_CLASSESS = 2
  23. IMAGE_WIDTH = 600
  24. IMAGE_HEIGHT = 800
  25. def vgg_net(weights, image):
  26. layers = (
  27. 'conv1_1', 'relu1_1', 'conv1_2', 'relu1_2', 'pool1',
  28. 'conv2_1', 'relu2_1', 'conv2_2', 'relu2_2', 'pool2',
  29. 'conv3_1', 'relu3_1', 'conv3_2', 'relu3_2', 'conv3_3',
  30. 'relu3_3', 'conv3_4', 'relu3_4', 'pool3',
  31. 'conv4_1', 'relu4_1', 'conv4_2', 'relu4_2', 'conv4_3',
  32. 'relu4_3', 'conv4_4', 'relu4_4', 'pool4',
  33. 'conv5_1', 'relu5_1', 'conv5_2', 'relu5_2', 'conv5_3',
  34. 'relu5_3', 'conv5_4', 'relu5_4'
  35. )
  36. net = {}
  37. current = image
  38. for i, name in enumerate(layers):
  39. if name in ['conv3_4', 'relu3_4', 'conv4_4', 'relu4_4', 'conv5_4', 'relu5_4']:
  40. continue
  41. kind = name[:4]
  42. if kind == 'conv':
  43. kernels, bias = weights[i][0][0][0][0]
  44. # matconvnet: weights are [width, height, in_channels, out_channels]
  45. # tensorflow: weights are [height, width, in_channels, out_channels]
  46. kernels = utils.get_variable(np.transpose(kernels, (1, 0, 2, 3)), name=name + "_w")
  47. bias = utils.get_variable(bias.reshape(-1), name=name + "_b")
  48. current = utils.conv2d_basic(current, kernels, bias)
  49. elif kind == 'relu':
  50. current = tf.nn.relu(current, name=name)
  51. if FLAGS.debug:
  52. utils.add_activation_summary(current)
  53. elif kind == 'pool':
  54. current = utils.avg_pool_2x2(current)
  55. net[name] = current
  56. return net
  57. def inference(image, keep_prob):
  58. """
  59. Semantic segmentation network definition
  60. :param image: input image. Should have values in range 0-255
  61. :param keep_prob:
  62. :return:
  63. """
  64. print("setting up vgg initialized conv layers ...")
  65. model_data = utils.get_model_data(FLAGS.model_dir, MODEL_URL)
  66. mean = model_data['normalization'][0][0][0]
  67. mean_pixel = np.mean(mean, axis=(0, 1))
  68. weights = np.squeeze(model_data['layers'])
  69. #processed_image = utils.process_image(image, mean_pixel)
  70. with tf.variable_scope("inference"):
  71. image_net = vgg_net(weights, image)
  72. conv_final_layer = image_net["conv5_3"]
  73. pool5 = utils.max_pool_2x2(conv_final_layer)
  74. W6 = utils.weight_variable([7, 7, 512, 4096], name="W6")
  75. b6 = utils.bias_variable([4096], name="b6")
  76. conv6 = utils.conv2d_basic(pool5, W6, b6)
  77. relu6 = tf.nn.relu(conv6, name="relu6")
  78. if FLAGS.debug:
  79. utils.add_activation_summary(relu6)
  80. relu_dropout6 = tf.nn.dropout(relu6, keep_prob=keep_prob)
  81. W7 = utils.weight_variable([1, 1, 4096, 4096], name="W7")
  82. b7 = utils.bias_variable([4096], name="b7")
  83. conv7 = utils.conv2d_basic(relu_dropout6, W7, b7)
  84. relu7 = tf.nn.relu(conv7, name="relu7")
  85. if FLAGS.debug:
  86. utils.add_activation_summary(relu7)
  87. relu_dropout7 = tf.nn.dropout(relu7, keep_prob=keep_prob)
  88. W8 = utils.weight_variable([1, 1, 4096, NUM_OF_CLASSESS], name="W8")
  89. b8 = utils.bias_variable([NUM_OF_CLASSESS], name="b8")
  90. conv8 = utils.conv2d_basic(relu_dropout7, W8, b8)
  91. # annotation_pred1 = tf.argmax(conv8, dimension=3, name="prediction1")
  92. # now to upscale to actual image size
  93. deconv_shape1 = image_net["pool4"].get_shape()
  94. W_t1 = utils.weight_variable([4, 4, deconv_shape1[3].value, NUM_OF_CLASSESS], name="W_t1")
  95. b_t1 = utils.bias_variable([deconv_shape1[3].value], name="b_t1")
  96. conv_t1 = utils.conv2d_transpose_strided(conv8, W_t1, b_t1, output_shape=tf.shape(image_net["pool4"]))
  97. fuse_1 = tf.add(conv_t1, image_net["pool4"], name="fuse_1")
  98. deconv_shape2 = image_net["pool3"].get_shape()
  99. W_t2 = utils.weight_variable([4, 4, deconv_shape2[3].value, deconv_shape1[3].value], name="W_t2")
  100. b_t2 = utils.bias_variable([deconv_shape2[3].value], name="b_t2")
  101. conv_t2 = utils.conv2d_transpose_strided(fuse_1, W_t2, b_t2, output_shape=tf.shape(image_net["pool3"]))
  102. fuse_2 = tf.add(conv_t2, image_net["pool3"], name="fuse_2")
  103. shape = tf.shape(image)
  104. deconv_shape3 = tf.stack([shape[0], shape[1], shape[2], NUM_OF_CLASSESS])
  105. W_t3 = utils.weight_variable([16, 16, NUM_OF_CLASSESS, deconv_shape2[3].value], name="W_t3")
  106. b_t3 = utils.bias_variable([NUM_OF_CLASSESS], name="b_t3")
  107. conv_t3 = utils.conv2d_transpose_strided(fuse_2, W_t3, b_t3, output_shape=deconv_shape3, stride=8)
  108. annotation_pred = tf.argmax(conv_t3, dimension=3, name="prediction")
  109. return tf.expand_dims(annotation_pred, dim=3), conv_t3
  110. def train(loss_val, var_list):
  111. optimizer = tf.train.AdamOptimizer(FLAGS.learning_rate)
  112. grads = optimizer.compute_gradients(loss_val, var_list=var_list)
  113. if FLAGS.debug:
  114. # print(len(var_list))
  115. for grad, var in grads:
  116. utils.add_gradient_summary(grad, var)
  117. return optimizer.apply_gradients(grads)
  118. def main(argv=None):
  119. keep_probability = tf.placeholder(tf.float32, name="keep_probabilty")
  120. image = tf.placeholder(tf.float32, shape=[None, IMAGE_HEIGHT, IMAGE_WIDTH, 6], name="input_image")
  121. annotation = tf.placeholder(tf.int32, shape=[None, IMAGE_HEIGHT, IMAGE_WIDTH, 1], name="annotation")
  122. pred_annotation, logits = inference(image, keep_probability)
  123. #tf.image_summary("input_image", image, max_images=2)
  124. #tf.image_summary("ground_truth", tf.cast(annotation, tf.uint8), max_images=2)
  125. #tf.image_summary("pred_annotation", tf.cast(pred_annotation, tf.uint8), max_images=2)
  126. loss = tf.reduce_mean((tf.nn.sparse_softmax_cross_entropy_with_logits(logits,
  127. tf.squeeze(annotation, squeeze_dims=[3]),
  128. name="entropy")))
  129. #tf.scalar_summary("entropy", loss)
  130. trainable_var = tf.trainable_variables()
  131. train_op = train(loss, trainable_var)
  132. #print("Setting up summary op...")
  133. #summary_op = tf.merge_all_summaries()
  134. '''''
  135. print("Setting up image reader...")
  136. train_records, valid_records = scene_parsing.read_dataset(FLAGS.data_dir)
  137. print(len(train_records))
  138. print(len(valid_records))
  139. print("Setting up dataset reader")
  140. image_options = {'resize': True, 'resize_size': IMAGE_SIZE}
  141. if FLAGS.mode == 'train':
  142. train_dataset_reader = dataset.BatchDatset(train_records, image_options)
  143. validation_dataset_reader = dataset.BatchDatset(valid_records, image_options)
  144. '''
  145. train_dataset_reader = BatchDatset('data/trainlist.mat')
  146. sess = tf.Session()
  147. print("Setting up Saver...")
  148. saver = tf.train.Saver()
  149. #summary_writer = tf.train.SummaryWriter(FLAGS.logs_dir, sess.graph)
  150. sess.run(tf.initialize_all_variables())
  151. ckpt = tf.train.get_checkpoint_state(FLAGS.logs_dir)
  152. if ckpt and ckpt.model_checkpoint_path:
  153. saver.restore(sess, ckpt.model_checkpoint_path)
  154. print("Model restored...")
  155. #if FLAGS.mode == "train":
  156. itr = 0
  157. train_images, train_annotations = train_dataset_reader.next_batch()
  158. trloss = 0.0
  159. while len(train_annotations) > 0:
  160. #train_images, train_annotations = train_dataset_reader.next_batch(FLAGS.batch_size)
  161. #print('==> batch data: ', train_images[0][100][100], '===', train_annotations[0][100][100])
  162. feed_dict = {image: train_images, annotation: train_annotations, keep_probability: 0.5}
  163. _, rloss =  sess.run([train_op, loss], feed_dict=feed_dict)
  164. trloss += rloss
  165. if itr % 100 == 0:
  166. #train_loss, rpred = sess.run([loss, pred_annotation], feed_dict=feed_dict)
  167. print("Step: %d, Train_loss:%f" % (itr, trloss / 100))
  168. trloss = 0.0
  169. #summary_writer.add_summary(summary_str, itr)
  170. #if itr % 10000 == 0 and itr > 0:
  171. '''''
  172. valid_images, valid_annotations = validation_dataset_reader.next_batch(FLAGS.batch_size)
  173. valid_loss = sess.run(loss, feed_dict={image: valid_images, annotation: valid_annotations,
  174. keep_probability: 1.0})
  175. print("%s ---> Validation_loss: %g" % (datetime.datetime.now(), valid_loss))'''
  176. itr += 1
  177. train_images, train_annotations = train_dataset_reader.next_batch()
  178. saver.save(sess, FLAGS.logs_dir + "plus_model.ckpt", itr)
  179. '''''elif FLAGS.mode == "visualize":
  180. valid_images, valid_annotations = validation_dataset_reader.get_random_batch(FLAGS.batch_size)
  181. pred = sess.run(pred_annotation, feed_dict={image: valid_images, annotation: valid_annotations,
  182. keep_probability: 1.0})
  183. valid_annotations = np.squeeze(valid_annotations, axis=3)
  184. pred = np.squeeze(pred, axis=3)
  185. for itr in range(FLAGS.batch_size):
  186. utils.save_image(valid_images[itr].astype(np.uint8), FLAGS.logs_dir, name="inp_" + str(5+itr))
  187. utils.save_image(valid_annotations[itr].astype(np.uint8), FLAGS.logs_dir, name="gt_" + str(5+itr))
  188. utils.save_image(pred[itr].astype(np.uint8), FLAGS.logs_dir, name="pred_" + str(5+itr))
  189. print("Saved image: %d" % itr)'''
  190. def pred():
  191. keep_probability = tf.placeholder(tf.float32, name="keep_probabilty")
  192. image = tf.placeholder(tf.float32, shape=[None, IMAGE_HEIGHT, IMAGE_WIDTH, 6], name="input_image")
  193. annotation = tf.placeholder(tf.int32, shape=[None, IMAGE_HEIGHT, IMAGE_WIDTH, 1], name="annotation")
  194. pred_annotation, logits = inference(image, keep_probability)
  195. sft = tf.nn.softmax(logits)
  196. test_dataset_reader = TestDataset('data/testlist.mat')
  197. with tf.Session() as sess:
  198. sess.run(tf.global_variables_initializer())
  199. ckpt = tf.train.get_checkpoint_state(FLAGS.logs_dir)
  200. saver = tf.train.Saver()
  201. if ckpt and ckpt.model_checkpoint_path:
  202. saver.restore(sess, ckpt.model_checkpoint_path)
  203. print("Model restored...")
  204. itr = 0
  205. test_images, test_annotations, test_orgs = test_dataset_reader.next_batch()
  206. #print('getting', test_annotations[0, 200:210, 200:210])
  207. while len(test_annotations) > 0:
  208. if itr < 22:
  209. test_images, test_annotations, test_orgs = test_dataset_reader.next_batch()
  210. itr += 1
  211. continue
  212. elif itr > 22:
  213. break
  214. feed_dict = {image: test_images, annotation: test_annotations, keep_probability: 0.5}
  215. rsft, pred_ann = sess.run([sft, pred_annotation], feed_dict=feed_dict)
  216. print(rsft.shape)
  217. _, h, w, _ = rsft.shape
  218. preds = np.zeros((h, w, 1), dtype=np.float)
  219. for i in range(h):
  220. for j in range(w):
  221. if rsft[0][i][j][0] < 0.1:
  222. preds[i][j][0] = 1.0
  223. elif rsft[0][i][j][0] < 0.9:
  224. preds[i][j][0] = 0.5
  225. else:
  226. preds[i][j]  = 0.0
  227. org0_im = Image.fromarray(np.uint8(test_orgs[0]))
  228. org0_im.save('res/org' + str(itr) + '.jpg')
  229. save_alpha_img(test_orgs[0], test_annotations[0], 'res/ann' + str(itr))
  230. save_alpha_img(test_orgs[0], preds, 'res/trimap' + str(itr))
  231. save_alpha_img(test_orgs[0], pred_ann[0], 'res/pre' + str(itr))
  232. test_images, test_annotations, test_orgs = test_dataset_reader.next_batch()
  233. itr += 1
  234. def save_alpha_img(org, mat, name):
  235. w, h = mat.shape[0], mat.shape[1]
  236. #print(mat[200:210, 200:210])
  237. rmat = np.reshape(mat, (w, h))
  238. amat = np.zeros((w, h, 4), dtype=np.int)
  239. amat[:, :, 3] = np.round(rmat * 1000)
  240. amat[:, :, 0:3] = org
  241. #print(amat[200:205, 200:205])
  242. #im = Image.fromarray(np.uint8(amat))
  243. #im.save(name + '.png')
  244. misc.imsave(name + '.png', amat)
  245. if __name__ == "__main__":
  246. #tf.app.run()
  247. pred()

到这里FCN+做人像分割已经讲完,当然本文的目的不单单是分割,还有分割之后的应用;

我们将训练数据扩充到人体分割,那么我们就是对人体做美颜特效处理,同时对背景做其他的特效处理,这样整张画面就会变得更加有趣,更加提高颜值了,这里我们对人体前景做美颜调色处理,对背景做了以下特效:

①景深模糊效果,用来模拟双摄聚焦效果;

②马赛克效果

③缩放模糊效果

④运动模糊效果

⑤油画效果

⑥线条漫画效果

⑦Glow梦幻效果

⑧铅笔画场景效果

⑨扩散效果

效果举例如下:

原图

人体分割MASK

景深模糊效果

马赛克效果

扩散效果

缩放模糊效果

运动模糊效果

油画效果

线条漫画效果

GLOW梦幻效果

铅笔画效果

最后给出DEMO链接:点击打开链接

本人QQ1358009172

基于抠图的人像特效算法相关推荐

  1. 深度学习AI美颜系列----基于抠图的人像特效算法

    美颜算法的重点在于美颜,也就是增加颜值,颜值的广定义,可以延伸到整个人体范围,也就是说,你的颜值不单单和你的脸有关系,还跟你穿什么衣服,什么鞋子相关,基于这个定义(这个定义是本人自己的说法,没有权威性 ...

  2. python实现面部特效_【AI美颜算法】300行Python实现基于人脸特征的美颜算法

    先上效果图: AI美颜 人类一直是一个看脸的物种,人人都希望可以变得更美是无可争议的,而美颜类应用的出现拯救了所有人,从此人类进入了美(照)颜(骗)时代. .... 每次写技术blog都要写一堆废话引 ...

  3. [Python从零到壹] 六十一.图像识别及经典案例篇之基于纹理背景和聚类算法的图像分割

    祝大家新年快乐,阖家幸福,健康快乐! 欢迎大家来到"Python从零到壹",在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界.所 ...

  4. 可租赁、可定制的虚拟人居然还能这么玩?9月25日来百度大脑人像特效专场一探究竟!...

    百度大脑自2016年启动开放以来,已打造成为业内最全面.最领先的AI开放平台,服务规模.调用量都居于业界第一. 百度大脑开放日于2019年开办,覆盖北/上/深等地区,成为众多AI开发者.合作伙伴近距离 ...

  5. 隆重介绍!一款新型基于姿势的人像实例分割框架

    全文共2493字,预计学习时长15分钟或更长 拍摄:Jezael Melgoza 来源:Unsplash 近年来,由于现实应用需求大,在计算机视觉领域有关"人"的研究层出不穷,实体 ...

  6. 由“娱乐主角”变“商业新军”,百度“人像特效”加速ToB

    文 | 曾响铃 来源 | 科技向令说(xiangling0815) 爱美之心人皆有之,变美之术层出不穷. 自2015年5月开始,被网友并称为"亚洲四大神术"的泰国变性术.韩国整容术 ...

  7. dbscan算法中 参数的意义_基于变参数的DBSCAN算法

    安全模型.算法与编程 |34| 基于变参数的 DBSCAN 算法 ◆付泽强 王晓锋 (江南大学物联网工程学院 江苏 214122) 摘要:DBSCAN 算法是一种常用的基于密度的聚类算法,其优点在于性 ...

  8. 基于微软开源深度学习算法,用 Python 实现图像和视频修复

    ‍‍ 作者 | 李秋键 编辑 | 夕颜 出品 | AI科技大本营(ID:rgznai100) 图像修复是计算机视觉领域的一个重要任务,在数字艺术品修复.公安刑侦面部修复等种种实际场景中被广泛应用.图像 ...

  9. 上交张伟楠副教授:基于模型的强化学习算法,基本原理以及前沿进展(附视频)

    2020 北京智源大会 本文属于2020北京智源大会嘉宾演讲的整理报道系列.北京智源大会是北京智源人工智能研究院主办的年度国际性人工智能高端学术交流活动,以国际性.权威性.专业性和前瞻性的" ...

最新文章

  1. 女面试官:我拉链开了你怎么提醒我?
  2. VS 的makefile工程
  3. flume数据丢失与重复_Flume架构及常见面试
  4. HDU 4609 3-idiots
  5. 数据中心机房设计及各专业技术平衡
  6. 【数论】能量采集(P1447)
  7. k2677场效应管参数引脚_共射极放大电路,场效应管放大电路,运算放大电路
  8. 利用bootstrap框架做了一个采摘节节日活动网页
  9. 边缘计算精华问答 | 5G是否会“逼退”4G?
  10. Java final 关键字简述
  11. 旭辉集团签约帆软软件,打造地产行业数字化转型新标杆
  12. 阿里巴巴Druid数据源及使用
  13. 盗企鹅号发娱乐八卦稿60天收益7.5万元?腾讯回应了...
  14. Android O新特性和行为变更总结zz
  15. win2016开启ntp_Windows Server 2016-Windows 时间服务概览
  16. Failed to create Anaconda Menus
  17. 基于JAVA学生会管理系统2021计算机毕业设计源码+系统+数据库+lw文档+部署
  18. 国产化服务器兼容系统,必须兼容中国芯,国产操作系统再迎来发展机遇?
  19. 宋图图的工学课程12
  20. 桌面应用程序UI框架有哪些

热门文章

  1. 华为IPSec高可靠性
  2. Vue路由点菜单关闭后打不开问题说明
  3. react动画_动画键盘(第2部分):对WindowInset动画做出React
  4. nmcli 命令配置网络
  5. 某东软件测试工程师(二面)——用例设计
  6. 用python求三角形周长文件_Python中最大的周长三角形
  7. outlook 日历共享_如何与他人共享Google日历
  8. api接口优化_使用电池状态API优化开发
  9. 前端大神博客收集大全
  10. JAVA线程安全Map解析