深度学习实战之手写签名识别(100%准确率、语音播报)

手写签名在日常生活中随处可见,简单来说就是亲笔书写自己的名字,在纸质文档上使用手写签名主要用以确定签字者的身份,并表示签字者同意所签署文档中规定的内容,对文档的真实性负责,且具有法律效力。由此看见手写签名的重要性。在现实的生活中不乏有不法分子模仿其他人的字体,进而模仿他人的签名获得不发的利益。尽管会有鉴别字体的工作,但在鉴别时不仅不准确,而且还十分的消耗人力以及财力。为了解决这一客观显示存在的问题,笔者结合着人工智能的思想和并使用计算机视觉技术对手写签名进行训练,得到了高达100%的训练准确率。并将训练模型进行优化后运用实现了一套手写签名识别系统。

1.开发环境

笔者的开发环境如下,大家可以参考进行配置

  • python3.6或python3.7
  • pytorch1.0.1
  • torchvision 0.2.2.post3
  • visom
  • ubuntu16.04和windows10

2. 准备阶段

在完成了上述的环境搭建后,即可进入到准备阶段了。这里准备的有数据集的准备、以及相关代码的主备。

  • 数据集的准备
    笔者这里的数据集是自己准备的,收集了六个人的手写签名,约4500张签名图片
    在这里插入图片描述
    在这里插入图片描述

  • 数据集的划分
    笔者这里将数据集进行六分类的划分,每一类约有750张图片
    数据集划分为训练数据、验证数据、测试数据
    其中训练数据、验证数据、测试数据的具体划分如下图所示
    在这里插入图片描述

  • 载入数据
    在上述的步骤中已经准备好了相应的数据集,同时也划分好了训练数据、验证数据、测试数据的部分。在完成了理论的设计后,该怎么使用代码对数据进行划分呢,代码如下所示

class Data(Dataset):
    def __init__(self,root,resize,mode):
        super(Data,self).__init__()
        # 保存参数
        self.root=root
        self.resize=resize
        # 给每一个类做映射
        self.name2label={}  # "daj":0 ,"hjj":1……
        for name in sorted(os.listdir(os.path.join(root))):
            # 过滤掉文件夹
            if not os.path.isdir(os.path.join(root,name)):
                continue
            # 保存在表中;将最长的映射作为最新的元素的label的值
            self.name2label[name]=len(self.name2label.keys())
        # print(self.name2label)
        # 加载文件
        self.images,self.labels=self.load_csv('images.csv')
        # 裁剪数据
        if mode=='train':
            self.images=self.images[:int(0.6*len(self.images))]   # 将数据集的60%设置为训练数据集合
            self.labels=self.labels[:int(0.6*len(self.labels))]   # label的60%分配给训练数据集合
        elif mode=='val':
            self.images = self.images[int(0.6 * len(self.images)):int(0.8 * len(self.images))]  # 从60%-80%的地方
            self.labels = self.labels[int(0.6 * len(self.labels)):int(0.8 * len(self.labels))]
        else:
            self.images = self.images[int(0.8 * len(self.images)):]   # 从80%的地方到最末尾
            self.labels = self.labels[int(0.8 * len(self.labels)):]
        # image+label 的路径

在使用上述的代码后即可将已有的数据集的60%划分为训练数据、20%划分为验证数据、20%划分为测试数据。

3. 训练阶段

在完成了上述的操作后,即可进入到训练阶段。这里笔者使用的是比较经典的神经网络结构--ResNet.
由何凯明团队所提出来的。其深度残差网络(Deep Residual Network)在2015年的ImageNet上取得冠军。具体网络的特点,读者可自行Google了解,这里笔者就不再赘述。

  • 搭建ResNet网络结构
    搭建的网络结构代码如下:
class ResBlk(nn.Module):

    def __init__(self, ch_in, ch_out, stride=1):
        super(ResBlk, self).__init__()

        self.conv1 = nn.Conv2d(ch_in, ch_out, kernel_size=3, stride=stride, padding=1)
        self.bn1 = nn.BatchNorm2d(ch_out)
        self.conv2 = nn.Conv2d(ch_out, ch_out, kernel_size=3, stride=1, padding=1)
        self.bn2 = nn.BatchNorm2d(ch_out)

        self.extra = nn.Sequential()
        if ch_out != ch_in:
            # [b, ch_in, h, w] => [b, ch_out, h, w]
            self.extra = nn.Sequential(
                nn.Conv2d(ch_in, ch_out, kernel_size=1, stride=stride),
                nn.BatchNorm2d(ch_out)
            )


    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        # short cut.
        # extra module: [b, ch_in, h, w] => [b, ch_out, h, w]
        # element-wise add:
        out = self.extra(x) + out
        out = F.relu(out)

        return out

class ResNet18(nn.Module):

    def __init__(self, num_class):
        super(ResNet18, self).__init__()

        self.conv1 = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, stride=3, padding=0),
            nn.BatchNorm2d(16)
        )
        # followed 4 blocks
        # [b, 16, h, w] => [b, 32, h ,w]
        self.blk1 = ResBlk(16, 32, stride=3)
        # [b, 32, h, w] => [b, 64, h, w]
        self.blk2 = ResBlk(32, 64, stride=3)
        # # [b, 64, h, w] => [b, 128, h, w]
        self.blk3 = ResBlk(64, 128, stride=2)
        # # [b, 128, h, w] => [b, 256, h, w]
        self.blk4 = ResBlk(128, 256, stride=2)

        # [b, 256, 7, 7]
        self.outlayer = nn.Linear(256*3*3, num_class)

    def forward(self, x):
        x = F.relu(self.conv1(x))

        # [b, 64, h, w] => [b, 1024, h, w]
        x = self.blk1(x)
        x = self.blk2(x)
        x = self.blk3(x)
        x = self.blk4(x)
        # print(x.shape)
        x = x.view(x.size(0), -1)
        x = self.outlayer(x)

        return x

def main():
    blk = ResBlk(64, 128)
    tmp = torch.randn(2, 64, 224, 224)
    out = blk(tmp)
    print('block:', out.shape)


    model = ResNet18(5)
    tmp = torch.randn(2, 3, 224, 224)
    out = model(tmp)
    print('resnet:', out.shape)

    p = sum(map(lambda p:p.numel(), model.parameters()))
    print('parameters size:', p)


if __name__ == '__main__':
    main()
  • 训练
    在完成了网络的搭建后,即可对数据进行训练。
    笔者这里设置如下
    batchsz=128
    lr = 1e-3
    epochs = 10
batchsz = 128
lr = 1e-3
epochs = 10

device = torch.device('cuda')
train_db = Data('train_data', 224, mode='train')
val_db = Data('train_data', 224, mode='val')
test_db = Data('train_data', 224, mode='test')
train_loader = DataLoader(train_db, batch_size=batchsz, shuffle=True,
                          num_workers=4)
val_loader = DataLoader(val_db, batch_size=batchsz, num_workers=4)
test_loader = DataLoader(test_db, batch_size=batchsz, num_workers=4)
viz = visdom.Visdom()

def evalute(model, loader):
    model.eval()
    correct = 0
    total = len(loader.dataset)

    for x, y in loader:
        x, y = x.to(device), y.to(device)
        with torch.no_grad():
            logits = model(x)
            pred = logits.argmax(dim=1)
        correct += torch.eq(pred, y).sum().float().item()

    return correct / total


def main():

    model = ResNet18(6).to(device)
    optimizer = optim.Adam(model.parameters(), lr=lr)
    criteon = nn.CrossEntropyLoss()

    best_acc, best_epoch = 0, 0
    global_step = 0
    viz.line([0], [-1], win='loss', opts=dict(title='loss'))
    viz.line([0], [-1], win='val_acc', opts=dict(title='val_acc'))
    for epoch in range(epochs):

        if epoch % 1 == 0:
            print('第 '+str(epoch+1)+' training……')
            val_acc = evalute(model, val_loader)
            if val_acc > best_acc:
                best_epoch = epoch
                best_acc = val_acc
                torch.save(model.state_dict(), 'best.mdl')
                viz.line([val_acc], [global_step], win='val_acc', update='append')

    print('最好的准确率:', best_acc, '最好的批次:', best_epoch)

    model.load_state_dict(torch.load('best.mdl'))
    print('正在加载模型……')

    test_acc = evalute(model, test_loader)
    print('测试准确率:', test_acc)

if __name__ == '__main__':
    main()

训练可视化结果如下:
在这里插入图片描述
这里也许读者会想有没有过拟合呢??
在这里插入图片描述
可以看到,也没有出现过拟合的问题。

4. 模型使用及系统实现

将训练获得的训练模型装载,并系统的使用其进行签名的识别。
这里笔者结合着计算机视觉常用的库opencv进行使用模型。
同时笔者为了能够更加符合日常的使用,这里笔者将opencv显示进行了中文化。
其代码如下:

def prediect(img_path):
    net=torch.load('model.pkl')
    net=net.to(device)
    torch.no_grad()
    img=Image.open(img_path)
    img=transform(img).unsqueeze(0)
    img_ = img.to(device)
    outputs = net(img_)
    _, predicted = torch.max(outputs, 1)
    print(classes[predicted[0]])

    begin = Speak()
    begin.speak(str(classes[predicted[0]]))

    img = cv2.imread(img_path)  # 如想读取中文名称的图片文件可用cv2.imdecode()
    pil_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # cv2和PIL中颜色的hex码的储存顺序不同,需转RGB模式
    pilimg = Image.fromarray(pil_img)  # Image.fromarray()将数组类型转成图片格式,与np.array()相反
    draw = ImageDraw.Draw(pilimg)  # PIL图片上打印汉字
    font = ImageFont.truetype("simhei.ttf", 20,
                              encoding="utf-8")  # 参数1:字体文件路径,参数2:字体大小
    draw.text((0, 0), classes[predicted[0]], (255, 0, 0), font=font)
    img = cv2.cvtColor(np.array(pilimg), cv2.COLOR_RGB2BGR)  # 将图片转成cv2.imshow()可以显示的数组格式
    cv2.imshow("show", img)
    if cv2.waitKey(0)==ord(' '):
        cv2.destroyAllWindows()

并使用系统进行实际的手写签名识别,其结果图下
在这里插入图片描述
同时在识别完成后,系统还会自动的将识别结果以语音的形式播报出来。

你听:澜江全 、狄爱景、陆春宇、王雅君……

最后申明:由于笔者知识水平有限,在问题描述上难免会有不准确的地方,还请大家谅解。希望大家多动手实践,共同进步。如若在实践的过程中出现问题,可以同我进行交流qq:1017190168

关注公众号“陶陶name”获取更多NLP和CV文章以及完整代码!

posted @ 2022-05-11 16:40  陶陶Name  阅读(1475)  评论(0编辑  收藏  举报