Kears+Opencv实现简单人脸识别


Posted in Python onAugust 28, 2019

写在前面:这篇文章也是借鉴了一些前辈的代码和思路写的,代码有些也是复用了别人的。

先说下思路:

1.首先利用Opencv检测出人脸的区域 

2.在成功的检测出人脸区域后,将识别出的人脸区域截取成图片,储存起来,用作后续的训练数据。

3.在获取到了足够多的数据后,搭建CNN网络进行训练。

5.训练完成后,将模型存储起来。 6.在利用Opencv实时读取视频流,将检测出人脸的区域变成图片放入模型中进行预测。

以上就是这个项目的一个基本的思路了。

1.利用Opencv检测人脸的代码如下,这个代码在Opencv的官方文档中也有,最重要的是加载的xml文件,因为这个xml文件中是保存这已经训练好的人脸检测模型。

import cv2
 
def identify_face(window_name, camera_idx):
  cv2.namedWindow(window_name)
 
  # 视频来源,可以来自一段已存好的视频,也可以直接来自USB摄像头
  cap = cv2.VideoCapture(camera_idx)
 
  # 告诉OpenCV使用人脸识别分类器
  classfier = cv2.CascadeClassifier("haarcascade_frontalface_alt2.xml")
 
  # 识别出人脸后要画的边框的颜色,RGB格式
  color = (0, 255, 0)
 
  while cap.isOpened(): # 是否初始化摄像头设备
    ok, frame = cap.read() # 读取一帧数据
    if not ok:
      break
 
    # 将当前帧转换成灰度图像
    grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
 
 
    # 人脸检测,1.2和4分别为图片缩放比例和需要检测的有效点数
    faceRects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=4, minSize=(32, 32))
    if len(faceRects) > 0: # 大于0则检测到人脸
      for faceRect in faceRects: # 单独框出每一张人脸
        x, y, w, h = faceRect  # 获得人脸的左上角对应坐标,及宽度和高度
        cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)
 
    # 显示图像
    cv2.imshow(window_name, frame)
    c = cv2.waitKey(10)
    if c & 0xFF == ord('q'):
      break
 
  # 释放摄像头并销毁所有窗口
  cap.release()
  cv2.destroyAllWindows()
 
 
if __name__ == '__main__':
  identify_face("identify face", 0)

2.在检测出人脸区域后,将人脸区域截取成图片保存下来

import cv2
from threading import Thread
 
 
 
def identify_face_and_store_face_image(window_name, camera_idx):
  cv2.namedWindow(window_name)
 
  # 视频来源,可以来自一段已存好的视频,也可以直接来自USB摄像头
  cap = cv2.VideoCapture(camera_idx)
 
  # 告诉OpenCV使用人脸识别分类器
  classfier = cv2.CascadeClassifier("haarcascade_frontalface_alt2.xml")
 
  # 识别出人脸后要画的边框的颜色,RGB格式
  color = (0, 255, 0)
 
 
  # 保存图片的索引
  num = 0
  while cap.isOpened(): # 是否初始化摄像头设备
    ok, frame = cap.read() # 读取一帧数据
    if not ok:
      break
 
    # 将当前帧转换成灰度图像
    grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
 
 
    # 人脸检测,1.2和2分别为图片缩放比例和需要检测的有效点数
    faceRects = classfier.detectMultiScale(grey, scaleFactor=1.2, minNeighbors=4, minSize=(32, 32))
    if len(faceRects) > 0: # 大于0则检测到人脸
      for faceRect in faceRects: # 单独框出每一张人脸
        x, y, w, h = faceRect  # 获得人脸的左上角对应坐标,及宽度和高度
 
        cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, 2)
 
        # store_face_image(frame, h, num, w, x, y)
        # 开启一个线程去存储人脸图片
        t = Thread(target=store_face_image, args=(frame, h, num, w, x, y, ))
        t.start()
 
 
        # 显示捕捉了多少张人脸照片
        font = cv2.FONT_HERSHEY_SIMPLEX # 字体
        cv2.putText(frame, ('num %d' % num), (x + 30, y + 30), font, 1, (255, 0, 255), 2)
 
        num += 1
        if num <= 1000: # 保存1000张图片后就退出
          break
 
 
    if num >= 1000:
      break
 
    # 显示图像
    cv2.imshow(window_name, frame)
    c = cv2.waitKey(10)
    if c & 0xFF == ord('q'):
      break
 
  # 释放摄像头并销毁所有窗口
  cap.release()
  cv2.destroyAllWindows()
 
 
def store_face_image(frame, h, num, w, x, y):
  # 将当前帧保存为图片
  img_name = '%s/%d.jpg' % (r'face_image', num)
  image = frame[y - 10: y + h + 10, x - 10: x + w + 10]
  cv2.imwrite(img_name, image)
 
if __name__ == '__main__':
  identify_face_and_store_face_image("identify face", 0)

3.将保存的数据进行处理,比如打上标签,进行归一化等等,下列代码是load_datasets.py文件中的

import os
import sys
import numpy as np
import cv2
 
IMAGE_SIZE = 64
 
 
# 按照指定图像大小调整尺寸
def resize_image(image, height=IMAGE_SIZE, width=IMAGE_SIZE):
  top, bottom, left, right = (0, 0, 0, 0)
 
  # 获取图像尺寸
  h, w, _ = image.shape
 
  # 对于长宽不相等的图片,找到最长的一边
  longest_edge = max(h, w)
 
  # 计算短边需要增加多上像素宽度使其与长边等长
  if h < longest_edge:
    dh = longest_edge - h
    top = dh // 2
    bottom = dh - top
  elif w < longest_edge:
    dw = longest_edge - w
    left = dw // 2
    right = dw - left
  else:
    pass
 
  # RGB颜色
  BLACK = [0, 0, 0]
 
  # 给图像增加边界,是图片长、宽等长,cv2.BORDER_CONSTANT指定边界颜色由value指定
  constant = cv2.copyMakeBorder(image, top, bottom, left, right, cv2.BORDER_CONSTANT, value=BLACK)
 
  # 调整图像大小并返回
  return cv2.resize(constant, (height, width))
 
 
# 读取训练数据
images = []
labels = []
 
 
def read_path(path_name):
  for dir_item in os.listdir(path_name):
    # 从初始路径开始叠加,合并成可识别的操作路径
    full_path = os.path.abspath(os.path.join(path_name, dir_item))
 
    if os.path.isdir(full_path): # 如果是文件夹,继续递归调用
      read_path(full_path)
    else: # 文件
      if dir_item.endswith('.jpg'):
        image = cv2.imread(full_path)
        image = resize_image(image, IMAGE_SIZE, IMAGE_SIZE)
 
        # 放开这个代码,可以看到resize_image()函数的实际调用效果
        # cv2.imwrite('1.jpg', image)
 
        images.append(image)
        labels.append(path_name)
 
  return images, labels
 
 
# 从指定路径读取训练数据
def load_dataset(path_name):
  images, labels = read_path(path_name)
 
  # 将输入的所有图片转成四维数组,尺寸为(图片数量*IMAGE_SIZE*IMAGE_SIZE*3)
  # 共587张图片,IMAGE_SIZE为64,故对我来说尺寸为587 * 64 * 64 * 3
  # 图片为64 * 64像素,一个像素3个颜色值(RGB)
  images = np.array(images)
  print(images.shape)
 
  # 标注数据,'my_face_image'文件夹下都是我的脸部图像,全部指定为0,你可以在获取他人的脸部图片比如说同学的,指定为1,
  labels = np.array([0 if label.endswith('my_face_image') else 1 for label in labels])
 
  return images, labels
 
 
if __name__ == '__main__':
  if len(sys.argv) != 1:
    print("Usage:%s path_name\r\n" % (sys.argv[0]))
  else:
    images, labels = load_dataset("face_image")

4.搭建模型,进行训练

import random
 
import numpy as np
from sklearn.model_selection import train_test_split
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Convolution2D, MaxPooling2D
from keras.optimizers import SGD
from keras.utils import np_utils
from keras.models import load_model
from keras import backend as K
 
from load_datasets import load_dataset, resize_image, IMAGE_SIZE
 
 
class Dataset:
  def __init__(self, path_name):
    # 训练集
    self.train_images = None
    self.train_labels = None
 
    # 验证集
    self.valid_images = None
    self.valid_labels = None
 
    # 测试集
    self.test_images = None
    self.test_labels = None
 
    # 数据集加载路径
    self.path_name = path_name
 
    # 当前库采用的维度顺序
    self.input_shape = None
 
  # 加载数据集并按照交叉验证的原则划分数据集并进行相关预处理工作
  def load(self, img_rows=IMAGE_SIZE, img_cols=IMAGE_SIZE,
       img_channels=3, nb_classes=2):
    # 加载数据集到内存
    images, labels = load_dataset(self.path_name)
 
    train_images, valid_images, train_labels, valid_labels = train_test_split(images, labels, test_size=0.3,
                                         random_state=random.randint(0, 100))
    _, test_images, _, test_labels = train_test_split(images, labels, test_size=0.5,
                             random_state=random.randint(0, 100))
 
    # 当前的维度顺序如果为'th',则输入图片数据时的顺序为:channels,rows,cols,否则:rows,cols,channels
    # 这部分代码就是根据keras库要求的维度顺序重组训练数据集
    if K.image_dim_ordering() == 'th':
      train_images = train_images.reshape(train_images.shape[0], img_channels, img_rows, img_cols)
      valid_images = valid_images.reshape(valid_images.shape[0], img_channels, img_rows, img_cols)
      test_images = test_images.reshape(test_images.shape[0], img_channels, img_rows, img_cols)
      self.input_shape = (img_channels, img_rows, img_cols)
    else:
      train_images = train_images.reshape(train_images.shape[0], img_rows, img_cols, img_channels)
      valid_images = valid_images.reshape(valid_images.shape[0], img_rows, img_cols, img_channels)
      test_images = test_images.reshape(test_images.shape[0], img_rows, img_cols, img_channels)
      self.input_shape = (img_rows, img_cols, img_channels)
 
      # 输出训练集、验证集、测试集的数量
      print(train_images.shape[0], 'train samples')
      print(valid_images.shape[0], 'valid samples')
      print(test_images.shape[0], 'test samples')
 
      # 我们的模型使用categorical_crossentropy作为损失函数,因此需要根据类别数量nb_classes将
      # 类别标签进行one-hot编码使其向量化,在这里我们的类别只有两种,经过转化后标签数据变为二维
      train_labels = np_utils.to_categorical(train_labels, nb_classes)
      valid_labels = np_utils.to_categorical(valid_labels, nb_classes)
      test_labels = np_utils.to_categorical(test_labels, nb_classes)
 
      # 像素数据浮点化以便归一化
      train_images = train_images.astype('float32')
      valid_images = valid_images.astype('float32')
      test_images = test_images.astype('float32')
 
      # 将其归一化,图像的各像素值归一化到0~1区间
      train_images /= 255
      valid_images /= 255
      test_images /= 255
 
      self.train_images = train_images
      self.valid_images = valid_images
      self.test_images = test_images
      self.train_labels = train_labels
      self.valid_labels = valid_labels
      self.test_labels = test_labels
 
 
# CNN网络模型类
class Model:
  def __init__(self):
    self.model = None
 
    # 建立模型
 
  def build_model(self, dataset, nb_classes=2):
    # 构建一个空的网络模型,它是一个线性堆叠模型,各神经网络层会被顺序添加,专业名称为序贯模型或线性堆叠模型
    self.model = Sequential()
 
    # 以下代码将顺序添加CNN网络需要的各层,一个add就是一个网络层
    self.model.add(Convolution2D(32, 3, 3, border_mode='same',
                   input_shape=dataset.input_shape)) # 1 2维卷积层
    self.model.add(Activation('relu')) # 2 激活函数层
 
    self.model.add(Convolution2D(32, 3, 3)) # 3 2维卷积层
    self.model.add(Activation('relu')) # 4 激活函数层
 
    self.model.add(MaxPooling2D(pool_size=(2, 2))) # 5 池化层
    self.model.add(Dropout(0.25)) # 6 Dropout层
 
    self.model.add(Convolution2D(64, 3, 3, border_mode='same')) # 7 2维卷积层
    self.model.add(Activation('relu')) # 8 激活函数层
 
    self.model.add(Convolution2D(64, 3, 3)) # 9 2维卷积层
    self.model.add(Activation('relu')) # 10 激活函数层
 
    self.model.add(MaxPooling2D(pool_size=(2, 2))) # 11 池化层
    self.model.add(Dropout(0.25)) # 12 Dropout层
 
    self.model.add(Flatten()) # 13 Flatten层
    self.model.add(Dense(512)) # 14 Dense层,又被称作全连接层
    self.model.add(Activation('relu')) # 15 激活函数层
    self.model.add(Dropout(0.5)) # 16 Dropout层
    self.model.add(Dense(nb_classes)) # 17 Dense层
    self.model.add(Activation('softmax')) # 18 分类层,输出最终结果
 
    # 输出模型概况
    self.model.summary()
 
  # 训练模型
  def train(self, dataset, batch_size=20, nb_epoch=10, data_augmentation=True):
    sgd = SGD(lr=0.01, decay=1e-6,
         momentum=0.9, nesterov=True) # 采用SGD+momentum的优化器进行训练,首先生成一个优化器对象
    self.model.compile(loss='categorical_crossentropy',
              optimizer=sgd,
              metrics=['accuracy']) # 完成实际的模型配置工作
 
    # 不使用数据提升,所谓的提升就是从我们提供的训练数据中利用旋转、翻转、加噪声等方法创造新的
    # 训练数据,有意识的提升训练数据规模,增加模型训练量
    if not data_augmentation:
      self.model.fit(dataset.train_images,
              dataset.train_labels,
              batch_size=batch_size,
              nb_epoch=nb_epoch,
              validation_data=(dataset.valid_images, dataset.valid_labels),
              shuffle=True)
    # 使用实时数据提升
    else:
      # 定义数据生成器用于数据提升,其返回一个生成器对象datagen,datagen每被调用一
      # 次其生成一组数据(顺序生成),节省内存,其实就是python的数据生成器
      datagen = ImageDataGenerator(
        featurewise_center=False, # 是否使输入数据去中心化(均值为0),
        samplewise_center=False, # 是否使输入数据的每个样本均值为0
        featurewise_std_normalization=False, # 是否数据标准化(输入数据除以数据集的标准差)
        samplewise_std_normalization=False, # 是否将每个样本数据除以自身的标准差
        zca_whitening=False, # 是否对输入数据施以ZCA白化
        rotation_range=20, # 数据提升时图片随机转动的角度(范围为0~180)
        width_shift_range=0.2, # 数据提升时图片水平偏移的幅度(单位为图片宽度的占比,0~1之间的浮点数)
        height_shift_range=0.2, # 同上,只不过这里是垂直
        horizontal_flip=True, # 是否进行随机水平翻转
        vertical_flip=False) # 是否进行随机垂直翻转
 
      # 计算整个训练样本集的数量以用于特征值归一化、ZCA白化等处理
      datagen.fit(dataset.train_images)
 
      # 利用生成器开始训练模型
      self.model.fit_generator(datagen.flow(dataset.train_images, dataset.train_labels,
                         batch_size=batch_size),
                   samples_per_epoch=dataset.train_images.shape[0],
                   nb_epoch=nb_epoch,
                   validation_data=(dataset.valid_images, dataset.valid_labels))
 
  MODEL_PATH = 'zhuhaipeng.face.model.h5'
 
  def save_model(self, file_path=MODEL_PATH):
    self.model.save(file_path)
 
  def load_model(self, file_path=MODEL_PATH):
    self.model = load_model(file_path)
 
  def evaluate(self, dataset):
    score = self.model.evaluate(dataset.test_images, dataset.test_labels, verbose=1)
    print("%s: %.2f%%" % (self.model.metrics_names[1], score[1] * 100))
 
  # 识别人脸
  def face_predict(self, image):
    # 依然是根据后端系统确定维度顺序
    if K.image_dim_ordering() == 'th' and image.shape != (1, 3, IMAGE_SIZE, IMAGE_SIZE):
      image = resize_image(image) # 尺寸必须与训练集一致都应该是IMAGE_SIZE x IMAGE_SIZE
      image = image.reshape((1, 3, IMAGE_SIZE, IMAGE_SIZE)) # 与模型训练不同,这次只是针对1张图片进行预测
    elif K.image_dim_ordering() == 'tf' and image.shape != (1, IMAGE_SIZE, IMAGE_SIZE, 3):
      image = resize_image(image)
      image = image.reshape((1, IMAGE_SIZE, IMAGE_SIZE, 3))
 
    # 浮点并归一化
    image = image.astype('float32')
    image /= 255
 
    # 给出输入属于各个类别的概率,我们是二值类别,则该函数会给出输入图像属于0和1的概率各为多少
    result = self.model.predict_proba(image)
    print('result:', result)
 
    # 给出类别预测:0或者1
    result = self.model.predict_classes(image)
 
    # 返回类别预测结果
    return result[0]
 
 
if __name__ == '__main__': # 训练模型
  dataset = Dataset('face_image')
  dataset.load()
 
  model = Model()
  model.build_model(dataset)
 
  # 测试训练函数的代码
  model.train(dataset)
 
if __name__ == '__main__': # 训练并保存模型
  dataset = Dataset('face_image')
  dataset.load()
 
  model = Model()
  model.build_model(dataset)
  model.train(dataset)
  model.save_model(file_path='model/zhuhaipeng.face.model.h5')
 
if __name__ == '__main__':  # 读取模型进行评估
  dataset = Dataset('face_image')
  dataset.load()
 
  # 评估模型
  model = Model()
  model.load_model(file_path='model/zhuhaipeng.face.model.h5')
  model.evaluate(dataset)

5.训练好模型后,使用Opencv实时读取视频流检测人脸位置,再将人脸放入模型中进行预测

# -*- coding: utf-8 -*-
 
import cv2
import sys
from face_train import Model
 
if __name__ == '__main__':
  if len(sys.argv) != 1:
    print("Usage:%s camera_id\r\n" % (sys.argv[0]))
    sys.exit(0)
 
  # 加载模型
  model = Model()
  model.load_model(file_path='model/zhuhaipeng.face.model.h5')
 
  # 框住人脸的矩形边框颜色
  color = (0, 255, 0)
 
  # 捕获指定摄像头的实时视频流
  cap = cv2.VideoCapture(0)
 
  # 人脸识别分类器本地存储路径
  cascade_path = "haarcascade_frontalface_alt2.xml"
 
  # 循环检测识别人脸
  while True:
    ret, frame = cap.read() # 读取一帧视频
 
    if ret is True:
 
      # 图像灰化,降低计算复杂度
      frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    else:
      continue
    # 使用人脸识别分类器,读入分类器
    cascade = cv2.CascadeClassifier(cascade_path)
 
    # 利用分类器识别出哪个区域为人脸
    faceRects = cascade.detectMultiScale(frame_gray, scaleFactor=1.2, minNeighbors=3, minSize=(32, 32))
    if len(faceRects) > 0:
      for faceRect in faceRects:
        x, y, w, h = faceRect
 
        # 截取脸部图像提交给模型识别这是谁
        image = frame[y - 10: y + h + 10, x - 10: x + w + 10]
        faceID = model.face_predict(image)
 
        # 如果是“我”
        if faceID == 0:
          cv2.rectangle(frame, (x - 10, y - 10), (x + w + 10, y + h + 10), color, thickness=2)
 
          # 文字提示是谁
          cv2.putText(frame, 'zhuhaipeng',
                (x + 30, y + 30), # 坐标
                cv2.FONT_HERSHEY_SIMPLEX, # 字体
                1, # 字号
                (255, 0, 255), # 颜色
                2) # 字的线宽
        else: # 如果不是我
          # 文字提示未知
          cv2.putText(frame, 'Unknown people ',
                (x + 30, y + 30), # 坐标
                cv2.FONT_HERSHEY_SIMPLEX, # 字体
                1, # 字号
                (255, 0, 255), # 颜色
                2) # 字的线宽
 
    cv2.imshow("identify me", frame)
 
    # 等待10毫秒看是否有按键输入
    k = cv2.waitKey(10)
    # 如果输入q则退出循环
    if k & 0xFF == ord('q'):
      break
 
  # 释放摄像头并销毁所有窗口
  cap.release()
  cv2.destroyAllWindows()

到此一个简单的人脸识别小项目就完成了,在这个项目中,只是简单的二分类,可以在此基础上进行扩展为多分类,如果识别的准确率低的话,可以尝试更改网络架构,或者对数据进行预处理等等,如果大家有兴趣改进的话,可以尝试一下。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python中readline判断文件读取结束的方法
Nov 08 Python
使用Python下的XSLT API进行web开发的简单教程
Apr 15 Python
Python编程之event对象的用法实例分析
Mar 23 Python
用Python设计一个经典小游戏
May 15 Python
Python编程实现从字典中提取子集的方法分析
Feb 09 Python
Python中装饰器学习总结
Feb 10 Python
Python装饰器用法实例总结
May 26 Python
pandas值替换方法
Jul 10 Python
pycharm使用matplotlib.pyplot不显示图形的解决方法
Oct 28 Python
Django之腾讯云短信的实现
Jun 12 Python
python UIAutomator2使用超详细教程
Feb 19 Python
python实现控制台输出颜色
Mar 02 Python
解决python 文本过滤和清理问题
Aug 28 #Python
解决Python对齐文本字符串问题
Aug 28 #Python
python+jinja2实现接口数据批量生成工具
Aug 28 #Python
解决Atom安装Hydrogen无法运行python3的问题
Aug 28 #Python
opencv实现简单人脸识别
Feb 19 #Python
Atom Python 配置Python3 解释器的方法
Aug 28 #Python
python聚类算法解决方案(rest接口/mpp数据库/json数据/下载图片及数据)
Aug 28 #Python
You might like
PHP setcookie设置Cookie用法(及设置无效的问题)
2011/07/13 PHP
php中把美国时间转为北京时间的自定义函数分享
2014/07/28 PHP
PHP模板引擎Smarty之配置文件在模板变量中的使用方法示例
2016/04/11 PHP
MAC下通过改apache配置文件切换php多版本的方法
2017/04/26 PHP
PHP正则判断一个变量是否为正整数的方法
2019/02/27 PHP
JS实现浏览器菜单命令
2006/09/05 Javascript
JavaScript 事件参考手册
2008/12/24 Javascript
学习ExtJS form布局
2009/10/08 Javascript
jquery网页元素拖拽插件效果及实现
2013/08/05 Javascript
jQuery插件实现控制网页元素动态居中显示
2015/03/24 Javascript
jQuery实现弹出窗口中切换登录与注册表单
2015/06/05 Javascript
JS延时器提示框的应用实例代码解析
2016/04/27 Javascript
AngularJS表单验证中级篇(3)
2016/09/28 Javascript
完美的js图片轮换效果
2017/02/05 Javascript
Node.js使用orm2进行update操作时关联字段无法修改的解决方法
2017/06/13 Javascript
详解Angular的8个主要构造块
2017/06/20 Javascript
jQuery实现动态添加节点与遍历节点功能示例
2017/11/09 jQuery
Vue + Elementui实现多标签页共存的方法
2019/06/12 Javascript
layer的prompt弹出框,点击回车,触发确定事件的方法
2019/09/06 Javascript
jdk1.8+vue elementui实现多级菜单功能
2020/09/24 Javascript
Nuxt.js的路由跳转操作(页面跳转nuxt-link)
2020/11/06 Javascript
python 中文乱码问题深入分析
2011/03/13 Python
编写Python脚本来获取Google搜索结果的示例
2015/05/04 Python
python实现按长宽比缩放图片
2018/06/07 Python
Python调用C++,通过Pybind11制作Python接口
2018/10/16 Python
python3实现二叉树的遍历与递归算法解析(小结)
2019/07/03 Python
pip指定python位置安装软件包的方法
2019/07/12 Python
一款纯css3实现的圆形旋转分享按钮旋转角度可自己调整
2014/09/02 HTML / CSS
巴西最大的珠宝连锁店:Vivara
2019/04/18 全球购物
测控技术与通信工程毕业生自荐信范文
2013/12/28 职场文书
感恩老师的演讲稿
2014/05/06 职场文书
大型活动组织方案
2014/05/10 职场文书
代领报检证委托书范本
2014/10/11 职场文书
2015年119消防宣传日活动总结
2015/03/24 职场文书
简单总结SpringMVC拦截器的使用方法
2021/06/28 Java/Android
Python合并pdf文件的工具
2021/07/01 Python