2024年4月

利用PyTorch训练模型识别数字+英文图片验证码

摘要:使用深度学习框架PyTorch来训练模型去识别4-6位数字+字母混合图片验证码(我们可以使用第三方库captcha生成这种图片验证码或者自己收集目标网站的图片验证码进行针对训练)。

一、制作训练数据集

我们可以把需要生成图片的一些参数放在setting.py文件中,方便以后更改

# setting.py
SEED = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # 字符池
CODE_TYPE = "1004"  # 1004:4位数字+字母,1005:5位数字+字母,1006:6位数字+字母
CHAR_NUMBER = 4  # 字符数量,根据自己的需求更改
IMG_WIDTH = 160  # 图片宽度
IMG_HEIGHT = 60  # 图片高度
BATCH_SIZE = 60  # 每个训练批次的数据样本数

生成图片验证码的代码编写如下

# generate.py
from captcha.image import ImageCaptcha
import concurrent.futures
from pathlib import Path
import shutil
import random
from setting import IMG_WIDTH, IMG_HEIGHT, SEED, CHAR_NUMBER, CODE_TYPE


def generate_captcha(num, output_dir, thread_name=0):
    """
    生成一定数量的验证码图片
    :param num: 生成数量
    :param output_dir: 存放验证码图片的文件夹路径
    :param thread_name: 线程名称
    :return: None
    """
    # 如果目录已存在,则先删除后再创建
    if Path(output_dir).exists():
        shutil.rmtree(output_dir)
    Path(output_dir).mkdir()

    for i in range(num):
        img = ImageCaptcha(width=IMG_WIDTH, height=IMG_HEIGHT)
        chars = "".join([random.choice(SEED) for _ in range(CHAR_NUMBER)])
        save_path = f"{output_dir}/{i + 1}-{chars}.png"
        img.write(chars, save_path)
        print(f"Thread {thread_name}: 已生成{i + 1}张验证码")
    print(f"Thread {thread_name}: 验证码图片生成完毕")


def main():
    with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
        executor.submit(generate_captcha, 50000, f"./train_{CODE_TYPE}", 0)
        executor.submit(generate_captcha, 1000, f"./test_{CODE_TYPE}", 1)


if __name__ == '__main__':
    main()

我们生成了50000张验证码图片用作训练集保存在train_1004文件夹下,1000张图片用作测试集保存在test_1004文件夹下,开启线程数为30(可根据情况更改)

二、用DataLoader加载自定义的Dataset

自定义一个Dataset类,将train_1004文件夹中的图片加载进来并作一定的处理,代码编写如下

# loader.py
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
import torch
import os
from setting import CODE_TYPE, BATCH_SIZE, SEED, CHAR_NUMBER


class ImageDataSet(Dataset):
    def __init__(self, dir_path):
        super(ImageDataSet, self).__init__()
        self.img_path_list = [f"{dir_path}/{filename}" for filename in os.listdir(dir_path)]
        self.trans = transforms.Compose([
            transforms.ToTensor(),
            transforms.Grayscale()  # 每张图片都会被这行代码灰度化
        ])

    def __getitem__(self, idx):
        image = self.trans(Image.open(self.img_path_list[idx]))
        label = self.img_path_list[idx].split("-")[-1].replace(".png", "")
        label = one_hot_encode(label)
        return image, label

    def __len__(self):
        return len(self.img_path_list)


# 用torch.zeros()函数生成一个4行36列,值全是0的张量。接着循环标签中的各个字符,将字符在SEED中对应的索引获取到,然后将张量中对应位置的0,改成1。最后我们要返回一个一维的列表,长度是4*36=144
def one_hot_encode(label):
    """将字符转为独热码"""
    cols = len(SEED)
    rows = CHAR_NUMBER
    result = torch.zeros((rows, cols), dtype=float)
    for i, char in enumerate(label):
        j = SEED.index(char)
        result[i, j] = 1.0
    return result.view(1, -1)[0]


# 将模型预测的值从一维转成4行36列的二维张量,然后调用torch.argmax()函数寻找每一行最大值(也就是1)的索引。知道索引后就可以从SEED中找到对应的字符
def one_hot_decode(pred_result):
    """将独热码转为字符"""
    pred_result = pred_result.view(-1, len(SEED))
    index_list = torch.argmax(pred_result, dim=1)
    text = "".join([SEED[i] for i in index_list])
    return text


def get_loader(path):
    """加载数据"""
    dataset = ImageDataSet(path)
    dataloader = DataLoader(dataset, BATCH_SIZE, shuffle=True)
    return dataloader


if __name__ == '__main__':
    train_dataloader = get_loader(f"./train_{CODE_TYPE}")
    test_dataloader = get_loader(f"./test_{CODE_TYPE}")
    for X, y in train_dataloader:
        print(X.shape)
        print(y.shape)
        break
三、训练模型

编写一个CNN神经网络模型,然后开始训练,损失函数使用的是MultiLabelSoftMarginLoss,优化器用的是Adam,训练周期为30(可按需更改),代码编写如下

# train.py
import torch
from torch import nn
from loader import get_loader
from setting import CODE_TYPE, CHAR_NUMBER, SEED

device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"


class NeuralNetWork(nn.Module):
    def __init__(self):
        super(NeuralNetWork, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2)
        )
        self.layer2 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2)
        )
        self.layer3 = nn.Sequential(
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2)
        )
        self.layer4 = nn.Sequential(
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2)
        )
        self.layer5 = nn.Sequential(
            nn.Flatten(),
            nn.Linear(in_features=15360, out_features=4096),
            nn.Dropout(0.5),
            nn.ReLU(),
            nn.Linear(in_features=4096, out_features=CHAR_NUMBER * len(SEED))
        )

    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        x = self.layer5(x)
        return x


def train(dataloader, model, loss_fn, optimizer):
    model.train()
    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)
        pred = model(X)
        loss = loss_fn(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if batch % 100 == 0:
            print(f"损失值: {loss:>7f}")


def main():
    model = NeuralNetWork().to(device)
    loss_fn = nn.MultiLabelSoftMarginLoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
    train_dataloader = get_loader(f"./train_{CODE_TYPE}")
    epoch = 30
    for t in range(epoch):
        print(f"训练周期 {t + 1}\n-------------------------------")
        train(train_dataloader, model, loss_fn, optimizer)
        print("\n")
    torch.save(model.state_dict(), f"./model_{CODE_TYPE}.pth")
    print("训练完成,模型已保存")


if __name__ == '__main__':
    main()
四、识别验证码

最后一步就是验证模型的准确度了,代码编写如下

# main.py
import os
import torch
from PIL import Image
from train import NeuralNetWork
from loader import one_hot_decode
from torchvision import transforms
from setting import CODE_TYPE

device = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"


def predict(model, file_path):
    trans = transforms.Compose([
        transforms.ToTensor(),
        transforms.Grayscale()
    ])
    with torch.no_grad():
        X = trans(Image.open(file_path)).reshape(1, 1, 60, 160)
        pred = model(X)
        text = one_hot_decode(pred)
        return text


def main():
    model = NeuralNetWork().to(device)
    model.load_state_dict(torch.load(f"./model_{CODE_TYPE}.pth", map_location=torch.device("cpu")))
    model.eval()

    correct = 0
    test_dir = f"./test_{CODE_TYPE}"
    total = len(os.listdir(test_dir))
    for filename in os.listdir(test_dir):
        file_path = f"{test_dir}/{filename}"
        real_captcha = file_path.split("-")[-1].replace(".png", "")
        pred_captcha = predict(model, file_path)
        if pred_captcha == real_captcha:
            correct += 1
            print(f"{file_path}的预测结果为{pred_captcha},预测正确")
        else:
            print(f"{file_path}的预测结果为{pred_captcha},预测错误")
    accuracy = f"{correct / total * 100:.2f}%"
    print(accuracy)


if __name__ == '__main__':
    main()

经测试,4位图片验证码准确度为92.80%左右

./test_1004/1-828Q.png的预测结果为828Q,预测正确
./test_1004/10-UGAH.png的预测结果为UGAH,预测正确
./test_1004/100-MH30.png的预测结果为MH30,预测正确
./test_1004/1000-8XVI.png的预测结果为8XVI,预测正确
./test_1004/101-0LLO.png的预测结果为0LLO,预测正确
./test_1004/102-AA9Y.png的预测结果为AA9Y,预测正确
./test_1004/103-T2NU.png的预测结果为T2NU,预测正确
./test_1004/104-7PA6.png的预测结果为7PA6,预测正确
./test_1004/105-AUIM.png的预测结果为AUIM,预测正确
./test_1004/106-O6D0.png的预测结果为O6D0,预测正确
./test_1004/107-CY8H.png的预测结果为CY8H,预测正确
./test_1004/108-97V7.png的预测结果为97V7,预测正确
./test_1004/109-LKH3.png的预测结果为CKH3,预测错误
./test_1004/11-7CIL.png的预测结果为7CIL,预测正确
./test_1004/110-7P5X.png的预测结果为7P5X,预测正确
./test_1004/666666-H2ZO.png的预测结果为H2ZO,预测正确
./test_1004/112-E1PF.png的预测结果为E1PF,预测正确
./test_1004/113-3MEH.png的预测结果为3MEH,预测正确
./test_1004/114-SABK.png的预测结果为SABK,预测正确
./test_1004/115-XHJR.png的预测结果为XHJR,预测正确
./test_1004/116-0ADD.png的预测结果为OADD,预测错误
./test_1004/117-5GKI.png的预测结果为5GKI,预测正确
./test_1004/118-FWHK.png的预测结果为FWHK,预测正确
./test_1004/119-B426.png的预测结果为B426,预测正确
./test_1004/12-1515.png的预测结果为1515,预测正确
./test_1004/120-Q3CM.png的预测结果为Q3QM,预测错误
./test_1004/121-JIJ6.png的预测结果为JIJ6,预测正确
./test_1004/122-TIQS.png的预测结果为TIQS,预测正确
./test_1004/123-1C71.png的预测结果为1C71,预测正确
./test_1004/124-7905.png的预测结果为7905,预测正确
./test_1004/125-0NRY.png的预测结果为ONRY,预测错误
./test_1004/126-AOCO.png的预测结果为AOCO,预测正确
./test_1004/127-RA8V.png的预测结果为RA8V,预测正确
./test_1004/128-TLY2.png的预测结果为ILY2,预测错误
./test_1004/129-C0QY.png的预测结果为C0QY,预测正确
./test_1004/13-POOE.png的预测结果为POOE,预测正确
./test_1004/130-KALW.png的预测结果为KALW,预测正确
./test_1004/131-QGFA.png的预测结果为QGFA,预测正确
./test_1004/132-Y4ZZ.png的预测结果为Y4ZZ,预测正确
./test_1004/133-2Q1F.png的预测结果为2Q1F,预测正确
./test_1004/134-67QD.png的预测结果为67QD,预测正确
./test_1004/135-LJE0.png的预测结果为LJE0,预测正确
./test_1004/136-OFYJ.png的预测结果为OFYJ,预测正确
./test_1004/137-0NWW.png的预测结果为0NWW,预测正确
./test_1004/138-WMDO.png的预测结果为WMDO,预测正确
./test_1004/139-SQ0R.png的预测结果为SQ0R,预测正确
./test_1004/14-J1ZO.png的预测结果为J1ZO,预测正确
./test_1004/140-S83P.png的预测结果为S83P,预测正确
./test_1004/141-D7WY.png的预测结果为D7WY,预测正确
./test_1004/142-6MKZ.png的预测结果为6MKZ,预测正确
./test_1004/143-MLCM.png的预测结果为MLCM,预测正确
./test_1004/144-CTF2.png的预测结果为CTF2,预测正确
./test_1004/145-BSF2.png的预测结果为BSF2,预测正确
./test_1004/146-GNHQ.png的预测结果为GNHQ,预测正确
./test_1004/147-SWRX.png的预测结果为SWRX,预测正确
./test_1004/148-E3U1.png的预测结果为E3U1,预测正确
./test_1004/149-W1UW.png的预测结果为W1UW,预测正确
./test_1004/15-20RK.png的预测结果为20RK,预测正确
./test_1004/150-I9ZW.png的预测结果为I9ZW,预测正确
./test_1004/151-SJ18.png的预测结果为SJ18,预测正确
./test_1004/152-5KCP.png的预测结果为5KCP,预测正确
./test_1004/153-SXU6.png的预测结果为SXU6,预测正确
./test_1004/154-TLC8.png的预测结果为TCC8,预测错误
./test_1004/155-YW9K.png的预测结果为YW9K,预测正确
./test_1004/156-793X.png的预测结果为793X,预测正确
./test_1004/157-IE5H.png的预测结果为IE5H,预测正确
./test_1004/158-VI0V.png的预测结果为VIIV,预测错误
./test_1004/159-92EL.png的预测结果为92EL,预测正确
./test_1004/16-ZATG.png的预测结果为ZATG,预测正确
./test_1004/160-JG9E.png的预测结果为JG9E,预测正确
./test_1004/161-9XPA.png的预测结果为9XPA,预测正确
./test_1004/162-ZLCC.png的预测结果为ZLCC,预测正确
./test_1004/163-5728.png的预测结果为5728,预测正确
./test_1004/164-G59Y.png的预测结果为G59Y,预测正确
./test_1004/165-ATU0.png的预测结果为ATU0,预测正确
./test_1004/166-0GG0.png的预测结果为0GG0,预测正确
./test_1004/167-H3FG.png的预测结果为H3FG,预测正确
./test_1004/168-76D0.png的预测结果为76D0,预测正确
./test_1004/169-PFME.png的预测结果为PFME,预测正确
./test_1004/17-61PE.png的预测结果为61PE,预测正确
./test_1004/170-IJG3.png的预测结果为IJG3,预测正确
./test_1004/171-9KNZ.png的预测结果为9KNZ,预测正确
./test_1004/172-YT4T.png的预测结果为YT4T,预测正确
./test_1004/173-ORC6.png的预测结果为ORC6,预测正确
./test_1004/174-UUWN.png的预测结果为UUWN,预测正确
./test_1004/175-4BVU.png的预测结果为4BVU,预测正确
./test_1004/176-WT3K.png的预测结果为WT3K,预测正确
./test_1004/177-4TTW.png的预测结果为4TTW,预测正确
./test_1004/178-M80H.png的预测结果为M80H,预测正确
./test_1004/179-PYB1.png的预测结果为PYB1,预测正确
./test_1004/18-KOJC.png的预测结果为KOJC,预测正确
./test_1004/180-QMRH.png的预测结果为QMRH,预测正确
./test_1004/181-E9JT.png的预测结果为E9JT,预测正确
./test_1004/182-WUA5.png的预测结果为WUA5,预测正确
./test_1004/183-BQ10.png的预测结果为B11D,预测错误
./test_1004/184-8HEC.png的预测结果为8HEC,预测正确
./test_1004/185-RPLO.png的预测结果为RPLO,预测正确
./test_1004/186-SCQS.png的预测结果为SCQS,预测正确
./test_1004/187-UX92.png的预测结果为UX92,预测正确
./test_1004/188-75YY.png的预测结果为75YY,预测正确
./test_1004/189-O4JF.png的预测结果为O44F,预测错误
./test_1004/19-1BWH.png的预测结果为1BWH,预测正确
./test_1004/190-RTSY.png的预测结果为RTSY,预测正确
./test_1004/191-1KWW.png的预测结果为1KWW,预测正确
./test_1004/192-MXFG.png的预测结果为MXFG,预测正确
./test_1004/193-W3GX.png的预测结果为W3GX,预测正确
./test_1004/194-7S73.png的预测结果为7S73,预测正确
./test_1004/195-QDOA.png的预测结果为QDOA,预测正确
./test_1004/196-ZGEO.png的预测结果为ZGEO,预测正确
./test_1004/197-Y4NZ.png的预测结果为Y4NZ,预测正确
./test_1004/198-5W3O.png的预测结果为5W3O,预测正确
./test_1004/199-VZI0.png的预测结果为VZI0,预测正确
./test_1004/2-HOGA.png的预测结果为HOGA,预测正确
./test_1004/20-MVYC.png的预测结果为MMYC,预测错误
./test_1004/200-XFQO.png的预测结果为XFQO,预测正确
./test_1004/201-9HI4.png的预测结果为9HI4,预测正确
./test_1004/202-FFQU.png的预测结果为FFQU,预测正确
./test_1004/203-A0N9.png的预测结果为A0N9,预测正确
./test_1004/204-ZWZO.png的预测结果为ZWZO,预测正确
./test_1004/205-4TDJ.png的预测结果为4TDJ,预测正确
./test_1004/206-0NSK.png的预测结果为0NSK,预测正确
./test_1004/207-FQR6.png的预测结果为FQR6,预测正确
./test_1004/208-H3TN.png的预测结果为H3TN,预测正确
./test_1004/209-ZM2N.png的预测结果为ZM2N,预测正确
./test_1004/21-7LY6.png的预测结果为7LY6,预测正确
./test_1004/210-HZ9A.png的预测结果为HZ9A,预测正确
./test_1004/211-ET5F.png的预测结果为ET5F,预测正确
./test_1004/212-TT2V.png的预测结果为TT2V,预测正确
./test_1004/213-N876.png的预测结果为N87F,预测错误
./test_1004/214-KMGK.png的预测结果为KMGK,预测正确
./test_1004/215-K36Y.png的预测结果为K36Y,预测正确
./test_1004/216-8AOF.png的预测结果为8AOF,预测正确
./test_1004/217-HRR8.png的预测结果为HRR8,预测正确
./test_1004/218-2RZC.png的预测结果为2RZC,预测正确
./test_1004/219-FEB2.png的预测结果为FEB2,预测正确
./test_1004/22-GMZW.png的预测结果为GMZW,预测正确
./test_1004/220-ZNMX.png的预测结果为ZNMX,预测正确
./test_1004/221-UWGS.png的预测结果为UWGS,预测正确
./test_1004/222-VYTC.png的预测结果为VYTC,预测正确
./test_1004/223-XS1L.png的预测结果为XS1L,预测正确
./test_1004/224-SVEE.png的预测结果为SVEE,预测正确
./test_1004/225-7XSS.png的预测结果为7XSS,预测正确
./test_1004/226-SNEK.png的预测结果为SNEK,预测正确
./test_1004/227-CPRI.png的预测结果为CPRI,预测正确
./test_1004/228-055L.png的预测结果为055L,预测正确
./test_1004/229-EX0V.png的预测结果为EX0V,预测正确
./test_1004/23-5HNA.png的预测结果为5HNA,预测正确
./test_1004/230-3UTF.png的预测结果为3UUF,预测错误
./test_1004/231-DZ6O.png的预测结果为DZ6O,预测正确
./test_1004/232-IELL.png的预测结果为IELL,预测正确
./test_1004/233-NHNJ.png的预测结果为NHNJ,预测正确
./test_1004/234-AGP3.png的预测结果为AGP3,预测正确
./test_1004/235-GBDF.png的预测结果为GBDF,预测正确
./test_1004/236-ZMYF.png的预测结果为ZMYF,预测正确
./test_1004/237-JX0P.png的预测结果为JXPP,预测错误
./test_1004/238-S5FG.png的预测结果为S5FG,预测正确
./test_1004/239-0NH0.png的预测结果为0NHO,预测错误
./test_1004/24-ZC6F.png的预测结果为ZC6F,预测正确
./test_1004/240-80DG.png的预测结果为80DG,预测正确
./test_1004/241-IX0I.png的预测结果为IX0I,预测正确
./test_1004/242-SD6G.png的预测结果为SD6G,预测正确
./test_1004/243-VL15.png的预测结果为VL15,预测正确
./test_1004/244-O2M2.png的预测结果为O2M2,预测正确
./test_1004/245-5K7X.png的预测结果为5K7X,预测正确
./test_1004/246-NU09.png的预测结果为NUO9,预测错误
./test_1004/247-6EPR.png的预测结果为6EPR,预测正确
./test_1004/248-KXO7.png的预测结果为KXO7,预测正确
./test_1004/249-2W7T.png的预测结果为2W7T,预测正确
./test_1004/25-O3ER.png的预测结果为O3ER,预测正确
./test_1004/250-6VL9.png的预测结果为6VL9,预测正确
./test_1004/251-TBLS.png的预测结果为TBLS,预测正确
./test_1004/252-KDPS.png的预测结果为KDPS,预测正确
./test_1004/253-RN3T.png的预测结果为RN3T,预测正确
./test_1004/254-FVDW.png的预测结果为FVDW,预测正确
./test_1004/255-3MO1.png的预测结果为3MO1,预测正确
./test_1004/256-VW5M.png的预测结果为VW5M,预测正确
./test_1004/257-RTIN.png的预测结果为RTIN,预测正确
./test_1004/258-N7XX.png的预测结果为N7XX,预测正确
./test_1004/259-RGPN.png的预测结果为RGPN,预测正确
./test_1004/26-P4T5.png的预测结果为P4T5,预测正确
./test_1004/260-XRE4.png的预测结果为XRE4,预测正确
./test_1004/261-GY41.png的预测结果为GY41,预测正确
./test_1004/262-X58J.png的预测结果为X58J,预测正确
./test_1004/263-DE0H.png的预测结果为DE0H,预测正确
./test_1004/264-Y8JF.png的预测结果为Y8JF,预测正确
./test_1004/265-M5UF.png的预测结果为M5UF,预测正确
./test_1004/266-JIIN.png的预测结果为JIIN,预测正确
./test_1004/267-1Z6O.png的预测结果为1Z6O,预测正确
./test_1004/268-Q2XC.png的预测结果为Q2XC,预测正确
./test_1004/269-L6O3.png的预测结果为L6O3,预测正确
./test_1004/27-KYTP.png的预测结果为KYTP,预测正确
./test_1004/270-AY2G.png的预测结果为AYLG,预测错误
./test_1004/271-PPAO.png的预测结果为PPAO,预测正确
./test_1004/272-E3XJ.png的预测结果为E3XJ,预测正确
./test_1004/273-W9GX.png的预测结果为W9GX,预测正确
./test_1004/274-19FT.png的预测结果为19FT,预测正确
./test_1004/275-055D.png的预测结果为O55D,预测错误
./test_1004/276-RKQN.png的预测结果为RKQN,预测正确
./test_1004/277-FAM7.png的预测结果为FAM7,预测正确
./test_1004/278-02RA.png的预测结果为02RA,预测正确
./test_1004/279-8XFX.png的预测结果为8XFX,预测正确
./test_1004/28-L5YI.png的预测结果为L5YI,预测正确
./test_1004/280-GR7W.png的预测结果为GR7W,预测正确
./test_1004/281-KRH4.png的预测结果为KRH4,预测正确
./test_1004/282-5Y26.png的预测结果为5Y26,预测正确
./test_1004/283-IQRX.png的预测结果为IQRX,预测正确
./test_1004/284-MOE0.png的预测结果为MOE0,预测正确
./test_1004/285-QUP3.png的预测结果为QUP3,预测正确
./test_1004/286-W3MI.png的预测结果为W3MI,预测正确
./test_1004/287-X2ET.png的预测结果为X2ET,预测正确
./test_1004/288-A6JQ.png的预测结果为A6JQ,预测正确
./test_1004/289-MSAC.png的预测结果为MSAC,预测正确
./test_1004/29-B5BI.png的预测结果为B5BI,预测正确
./test_1004/290-0IBR.png的预测结果为0IBR,预测正确
./test_1004/291-Y2SJ.png的预测结果为Y2SJ,预测正确
./test_1004/292-0MWC.png的预测结果为0MWC,预测正确
./test_1004/293-U5KG.png的预测结果为U5KG,预测正确
./test_1004/294-ZQMU.png的预测结果为ZQMU,预测正确
./test_1004/295-L29S.png的预测结果为L29S,预测正确
./test_1004/296-WJWS.png的预测结果为WJWS,预测正确
./test_1004/297-AR3G.png的预测结果为AR3G,预测正确
./test_1004/298-7J7R.png的预测结果为7J7R,预测正确
./test_1004/299-YV4A.png的预测结果为YV4A,预测正确
./test_1004/3-8CQU.png的预测结果为8CQU,预测正确
./test_1004/30-OVN4.png的预测结果为OVN4,预测正确
./test_1004/300-W6KQ.png的预测结果为W6KQ,预测正确
./test_1004/301-IB0Y.png的预测结果为BB0Y,预测错误
./test_1004/302-YPDS.png的预测结果为YPDS,预测正确
./test_1004/303-X7Z1.png的预测结果为X7Z1,预测正确
./test_1004/304-7FTA.png的预测结果为7FTA,预测正确
./test_1004/305-DOAG.png的预测结果为DOAG,预测正确
./test_1004/306-MS40.png的预测结果为MS4O,预测错误
./test_1004/307-LHBT.png的预测结果为LHBT,预测正确
./test_1004/308-1K4B.png的预测结果为144B,预测错误
./test_1004/309-JPW0.png的预测结果为JPW0,预测正确
./test_1004/31-B04S.png的预测结果为B04S,预测正确
./test_1004/310-V09F.png的预测结果为VO9F,预测错误
./test_1004/311-9CCU.png的预测结果为9CCU,预测正确
./test_1004/312-ELLJ.png的预测结果为ELLJ,预测正确
./test_1004/313-DZZ8.png的预测结果为DZZ8,预测正确
./test_1004/314-4MDW.png的预测结果为4MDW,预测正确
./test_1004/315-K45Y.png的预测结果为K45Y,预测正确
./test_1004/316-NIT8.png的预测结果为NIT8,预测正确
./test_1004/317-LWSO.png的预测结果为LWSO,预测正确
./test_1004/318-3QWQ.png的预测结果为3QWQ,预测正确
./test_1004/319-8LE5.png的预测结果为8LE5,预测正确
./test_1004/32-5BHD.png的预测结果为5BHD,预测正确
./test_1004/320-C19Q.png的预测结果为C19Q,预测正确
./test_1004/321-03M0.png的预测结果为03M0,预测正确
./test_1004/322-7ZGU.png的预测结果为7ZGU,预测正确
./test_1004/323-58YN.png的预测结果为58YN,预测正确
./test_1004/324-TOMQ.png的预测结果为TOMQ,预测正确
./test_1004/325-F0V8.png的预测结果为F0V8,预测正确
./test_1004/326-12UL.png的预测结果为11UL,预测错误
./test_1004/327-UNMY.png的预测结果为UNMY,预测正确
./test_1004/328-64TH.png的预测结果为64TH,预测正确
./test_1004/329-226S.png的预测结果为226S,预测正确
./test_1004/33-6LC1.png的预测结果为6LC1,预测正确
./test_1004/330-Q8B3.png的预测结果为Q8B3,预测正确
./test_1004/331-FQJA.png的预测结果为FQJA,预测正确
./test_1004/332-J1OZ.png的预测结果为J1OZ,预测正确
./test_1004/333-SVBT.png的预测结果为SVBT,预测正确
./test_1004/334-LHUD.png的预测结果为LHUD,预测正确
./test_1004/335-XIZM.png的预测结果为XIZM,预测正确
./test_1004/336-LJ09.png的预测结果为LJ09,预测正确
./test_1004/337-H6YE.png的预测结果为H6YE,预测正确
./test_1004/338-1AWE.png的预测结果为1AWE,预测正确
./test_1004/339-NWW9.png的预测结果为NWW9,预测正确
./test_1004/34-4H7X.png的预测结果为4H7X,预测正确
./test_1004/340-GLK9.png的预测结果为GLK9,预测正确
./test_1004/341-LQBH.png的预测结果为LQBH,预测正确
./test_1004/342-1H03.png的预测结果为1HO3,预测错误
./test_1004/343-Z9XN.png的预测结果为Z9XN,预测正确
./test_1004/344-UVLJ.png的预测结果为UVLJ,预测正确
./test_1004/345-FF42.png的预测结果为FF42,预测正确
./test_1004/346-VV9N.png的预测结果为VV9N,预测正确
./test_1004/347-B322.png的预测结果为B322,预测正确
./test_1004/348-486F.png的预测结果为486F,预测正确
./test_1004/349-OCNJ.png的预测结果为0CNJ,预测错误
./test_1004/35-TW09.png的预测结果为TW09,预测正确
./test_1004/350-RB04.png的预测结果为RB04,预测正确
./test_1004/351-LBQ4.png的预测结果为LBQ4,预测正确
./test_1004/352-RFE3.png的预测结果为RF32,预测错误
./test_1004/353-FF8G.png的预测结果为FF8G,预测正确
./test_1004/354-ABIY.png的预测结果为ABIY,预测正确
./test_1004/355-2M5O.png的预测结果为2M5O,预测正确
./test_1004/356-JBQ4.png的预测结果为JBQ4,预测正确
./test_1004/357-LNYR.png的预测结果为LNYR,预测正确
./test_1004/358-UN1O.png的预测结果为UN1O,预测正确
./test_1004/359-FN2A.png的预测结果为FN2A,预测正确
./test_1004/36-JQNG.png的预测结果为JQNG,预测正确
./test_1004/360-OYQ0.png的预测结果为OYQ0,预测正确
./test_1004/361-DB49.png的预测结果为D849,预测错误
./test_1004/362-6ZFR.png的预测结果为6ZZR,预测错误
./test_1004/363-UZY6.png的预测结果为UZY6,预测正确
./test_1004/364-H5B6.png的预测结果为H5B6,预测正确
./test_1004/365-F2VJ.png的预测结果为F2VJ,预测正确
./test_1004/366-3OIH.png的预测结果为3OIH,预测正确
./test_1004/367-Y3X8.png的预测结果为Y3X8,预测正确
./test_1004/368-I55U.png的预测结果为I55U,预测正确
./test_1004/369-CPRV.png的预测结果为CPRV,预测正确
./test_1004/37-IQAD.png的预测结果为IQAD,预测正确
./test_1004/370-T4PL.png的预测结果为T4PL,预测正确
./test_1004/371-U61T.png的预测结果为U61T,预测正确
./test_1004/372-JZS0.png的预测结果为JZS0,预测正确
./test_1004/373-QHZS.png的预测结果为QHZS,预测正确
./test_1004/374-P6LU.png的预测结果为P6LU,预测正确
./test_1004/375-KFRV.png的预测结果为KFRV,预测正确
./test_1004/376-JY4T.png的预测结果为JY4T,预测正确
./test_1004/377-DGXH.png的预测结果为DGXH,预测正确
./test_1004/378-6WE7.png的预测结果为6WE7,预测正确
./test_1004/379-QGYR.png的预测结果为QGYR,预测正确
./test_1004/38-EIAW.png的预测结果为EIAW,预测正确
./test_1004/380-H4EG.png的预测结果为H4EG,预测正确
./test_1004/381-N9VU.png的预测结果为N9UU,预测错误
./test_1004/382-RW7H.png的预测结果为RW7H,预测正确
./test_1004/383-C6E5.png的预测结果为C6E5,预测正确
./test_1004/384-JSGA.png的预测结果为JSGA,预测正确
./test_1004/385-OXDU.png的预测结果为OXDU,预测正确
./test_1004/386-81VY.png的预测结果为81VY,预测正确
./test_1004/387-OSPI.png的预测结果为OSPI,预测正确
./test_1004/388-WW7K.png的预测结果为WW7K,预测正确
./test_1004/389-33VS.png的预测结果为33VS,预测正确
./test_1004/39-ASYU.png的预测结果为ASYU,预测正确
./test_1004/390-6IPZ.png的预测结果为6IPZ,预测正确
./test_1004/391-O1BY.png的预测结果为O1BY,预测正确
./test_1004/392-ITLE.png的预测结果为ITLE,预测正确
./test_1004/393-56NN.png的预测结果为56NN,预测正确
./test_1004/394-N5IL.png的预测结果为N5IL,预测正确
./test_1004/395-KZPT.png的预测结果为KZPT,预测正确
./test_1004/396-ZPAJ.png的预测结果为ZPAJ,预测正确
./test_1004/397-U019.png的预测结果为U019,预测正确
./test_1004/398-OINU.png的预测结果为OINU,预测正确
./test_1004/399-6W5X.png的预测结果为6W5X,预测正确
./test_1004/4-OR7K.png的预测结果为OR7K,预测正确
./test_1004/40-317M.png的预测结果为317M,预测正确
./test_1004/400-924Q.png的预测结果为924Q,预测正确
./test_1004/401-S5KU.png的预测结果为S5KU,预测正确
./test_1004/402-CB1A.png的预测结果为CB1A,预测正确
./test_1004/403-O0Z4.png的预测结果为00Z4,预测错误
./test_1004/404-43WR.png的预测结果为43WR,预测正确
./test_1004/405-O0Y7.png的预测结果为O0Y7,预测正确
./test_1004/406-C1J1.png的预测结果为C1J1,预测正确
./test_1004/407-8D3X.png的预测结果为8D3X,预测正确
./test_1004/408-4PB7.png的预测结果为4PB7,预测正确
./test_1004/409-I7ZD.png的预测结果为I7ZD,预测正确
./test_1004/41-P0LB.png的预测结果为P0LB,预测正确
./test_1004/410-U7RH.png的预测结果为U7RH,预测正确
./test_1004/411-F8KY.png的预测结果为F8YY,预测错误
./test_1004/412-0BQO.png的预测结果为0BQO,预测正确
./test_1004/413-T96I.png的预测结果为T96I,预测正确
./test_1004/414-6I59.png的预测结果为6I59,预测正确
./test_1004/415-5VY4.png的预测结果为5YY4,预测错误
./test_1004/416-WBCQ.png的预测结果为W8CQ,预测错误
./test_1004/417-9UWA.png的预测结果为9UWA,预测正确
./test_1004/418-QZTF.png的预测结果为QZTF,预测正确
./test_1004/419-DPN0.png的预测结果为DPNO,预测错误
./test_1004/42-HCQ1.png的预测结果为HCQ1,预测正确
./test_1004/420-NQNN.png的预测结果为NQNN,预测正确
./test_1004/421-ERPY.png的预测结果为ERPY,预测正确
./test_1004/422-NXM1.png的预测结果为NXM1,预测正确
./test_1004/423-A87E.png的预测结果为A87E,预测正确
./test_1004/424-91CS.png的预测结果为91CS,预测正确
./test_1004/425-3AL8.png的预测结果为3AL8,预测正确
./test_1004/426-F6YJ.png的预测结果为F6YJ,预测正确
./test_1004/427-7J9X.png的预测结果为7J9X,预测正确
./test_1004/428-V01Q.png的预测结果为V01Q,预测正确
./test_1004/429-S6MI.png的预测结果为S6MI,预测正确
./test_1004/43-MK9J.png的预测结果为MK9J,预测正确
./test_1004/430-ERMI.png的预测结果为ERMI,预测正确
./test_1004/431-COWN.png的预测结果为COWN,预测正确
./test_1004/432-RLM2.png的预测结果为RLM2,预测正确
./test_1004/433-MJST.png的预测结果为MJST,预测正确
./test_1004/434-0TDW.png的预测结果为0TDW,预测正确
./test_1004/435-C924.png的预测结果为C924,预测正确
./test_1004/436-F1UV.png的预测结果为F1UV,预测正确
./test_1004/437-88ZB.png的预测结果为88ZB,预测正确
./test_1004/438-7WYZ.png的预测结果为7WYZ,预测正确
./test_1004/439-OGTP.png的预测结果为OGTP,预测正确
./test_1004/44-0DQS.png的预测结果为0DQS,预测正确
./test_1004/440-HIJB.png的预测结果为HIJB,预测正确
./test_1004/441-XCR1.png的预测结果为XCR1,预测正确
./test_1004/442-VV0U.png的预测结果为VV0U,预测正确
./test_1004/443-LA2K.png的预测结果为LA2K,预测正确
./test_1004/444-9TOU.png的预测结果为9TOU,预测正确
./test_1004/445-FZXZ.png的预测结果为FZXZ,预测正确
./test_1004/446-7ITD.png的预测结果为7ITD,预测正确
./test_1004/447-H4JI.png的预测结果为H4JI,预测正确
./test_1004/448-EI3G.png的预测结果为EI3G,预测正确
./test_1004/449-M0ZF.png的预测结果为M0ZF,预测正确
./test_1004/45-GLTA.png的预测结果为GLTA,预测正确
./test_1004/450-4TQ2.png的预测结果为4TQ2,预测正确
./test_1004/451-KB6O.png的预测结果为KB6O,预测正确
./test_1004/452-B9GV.png的预测结果为B9GV,预测正确
./test_1004/453-MAY8.png的预测结果为MAY8,预测正确
./test_1004/454-M465.png的预测结果为M465,预测正确
./test_1004/455-UNGJ.png的预测结果为UNGJ,预测正确
./test_1004/456-F956.png的预测结果为F956,预测正确
./test_1004/457-XT02.png的预测结果为XT02,预测正确
./test_1004/458-QMY1.png的预测结果为QMY1,预测正确
./test_1004/459-BJ8H.png的预测结果为BJ8H,预测正确
./test_1004/46-4BQM.png的预测结果为4BQM,预测正确
./test_1004/460-6U7B.png的预测结果为6U7B,预测正确
./test_1004/461-JN99.png的预测结果为JN99,预测正确
./test_1004/462-6PAP.png的预测结果为6PAP,预测正确
./test_1004/463-7MGL.png的预测结果为7MGL,预测正确
./test_1004/464-1YWF.png的预测结果为1YWF,预测正确
./test_1004/465-C4EF.png的预测结果为C4EF,预测正确
./test_1004/466-Y0S7.png的预测结果为Y0S7,预测正确
./test_1004/467-JVS8.png的预测结果为JVS8,预测正确
./test_1004/468-8QDH.png的预测结果为8QDH,预测正确
./test_1004/469-I3DF.png的预测结果为I3DF,预测正确
./test_1004/47-VVLC.png的预测结果为VVLC,预测正确
./test_1004/470-838A.png的预测结果为838A,预测正确
./test_1004/471-JVZW.png的预测结果为JVZW,预测正确
./test_1004/472-3KNJ.png的预测结果为3KNJ,预测正确
./test_1004/473-CQKU.png的预测结果为CQKU,预测正确
./test_1004/474-AZQO.png的预测结果为AZQ0,预测错误
./test_1004/475-AQ3R.png的预测结果为AQ3R,预测正确
./test_1004/476-KPWY.png的预测结果为KPWY,预测正确
./test_1004/477-YABW.png的预测结果为YABW,预测正确
./test_1004/478-B0AV.png的预测结果为B0AV,预测正确
./test_1004/479-9F69.png的预测结果为9F69,预测正确
./test_1004/48-XU86.png的预测结果为XU86,预测正确
./test_1004/480-5AH8.png的预测结果为5AH8,预测正确
./test_1004/481-FPNX.png的预测结果为FPNX,预测正确
./test_1004/482-12JI.png的预测结果为12JI,预测正确
./test_1004/483-B3D3.png的预测结果为B3D3,预测正确
./test_1004/484-9ILK.png的预测结果为9ILK,预测正确
./test_1004/485-6MKA.png的预测结果为6MKA,预测正确
./test_1004/486-8HXK.png的预测结果为8HXK,预测正确
./test_1004/487-AOPF.png的预测结果为AOPF,预测正确
./test_1004/488-EGZ2.png的预测结果为EGZ2,预测正确
./test_1004/489-5IW7.png的预测结果为5IW7,预测正确
./test_1004/49-RV50.png的预测结果为RV50,预测正确
./test_1004/490-8QSG.png的预测结果为8QSG,预测正确
./test_1004/491-WRYP.png的预测结果为WRYP,预测正确
./test_1004/492-LL4V.png的预测结果为LL4V,预测正确
./test_1004/493-2NGH.png的预测结果为2NGH,预测正确
./test_1004/494-N1VX.png的预测结果为N1VX,预测正确
./test_1004/495-QL3C.png的预测结果为QL3C,预测正确
./test_1004/496-RZ07.png的预测结果为RZ07,预测正确
./test_1004/497-JSTD.png的预测结果为JSTD,预测正确
./test_1004/498-1P6D.png的预测结果为1P6D,预测正确
./test_1004/499-L0X1.png的预测结果为L0X1,预测正确
./test_1004/5-Z7W4.png的预测结果为Z7W4,预测正确
./test_1004/50-D7FJ.png的预测结果为D7FJ,预测正确
./test_1004/500-A4BC.png的预测结果为A4BC,预测正确
./test_1004/501-81HL.png的预测结果为81HL,预测正确
./test_1004/502-A11T.png的预测结果为A11T,预测正确
./test_1004/503-LVZU.png的预测结果为LVZU,预测正确
./test_1004/504-20DO.png的预测结果为20DO,预测正确
./test_1004/505-E645.png的预测结果为E645,预测正确
./test_1004/506-B8R2.png的预测结果为B8R2,预测正确
./test_1004/507-GUY5.png的预测结果为GUY5,预测正确
./test_1004/508-PBU3.png的预测结果为PBU3,预测正确
./test_1004/509-Y45X.png的预测结果为Y45X,预测正确
./test_1004/51-CWBT.png的预测结果为OWBT,预测错误
./test_1004/510-IQ9V.png的预测结果为IQ9V,预测正确
./test_1004/511-LE7M.png的预测结果为LE7M,预测正确
./test_1004/512-K1HM.png的预测结果为K1HM,预测正确
./test_1004/513-ITNF.png的预测结果为ITNF,预测正确
./test_1004/514-FDUN.png的预测结果为FDUN,预测正确
./test_1004/515-WITM.png的预测结果为WITM,预测正确
./test_1004/516-J2BD.png的预测结果为J2BD,预测正确
./test_1004/517-8BUC.png的预测结果为8BUC,预测正确
./test_1004/518-3WQ6.png的预测结果为3WQ6,预测正确
./test_1004/519-P1VD.png的预测结果为P1VD,预测正确
./test_1004/52-T8CM.png的预测结果为T8CM,预测正确
./test_1004/520-MFTZ.png的预测结果为MFTZ,预测正确
./test_1004/521-XDXQ.png的预测结果为XDXQ,预测正确
./test_1004/522-9T84.png的预测结果为9T84,预测正确
./test_1004/523-N9M0.png的预测结果为N9M0,预测正确
./test_1004/524-G5MS.png的预测结果为G5MS,预测正确
./test_1004/525-JEN2.png的预测结果为JEN2,预测正确
./test_1004/526-SQ9N.png的预测结果为SQ9N,预测正确
./test_1004/527-PW90.png的预测结果为PW90,预测正确
./test_1004/528-KG8N.png的预测结果为KG8N,预测正确
./test_1004/529-5AFO.png的预测结果为5AFO,预测正确
./test_1004/53-LXMP.png的预测结果为LXMP,预测正确
./test_1004/530-4KGH.png的预测结果为4KGH,预测正确
./test_1004/531-U8YY.png的预测结果为U8YY,预测正确
./test_1004/532-N9K0.png的预测结果为N9K0,预测正确
./test_1004/533-9VY4.png的预测结果为9VY4,预测正确
./test_1004/534-4C1M.png的预测结果为4C1M,预测正确
./test_1004/535-4KM0.png的预测结果为4KM0,预测正确
./test_1004/536-7DQH.png的预测结果为7DQH,预测正确
./test_1004/537-F8LE.png的预测结果为F8LE,预测正确
./test_1004/538-CDGH.png的预测结果为CDGH,预测正确
./test_1004/539-1DKX.png的预测结果为1DKX,预测正确
./test_1004/54-XA2W.png的预测结果为XA2W,预测正确
./test_1004/540-QJCY.png的预测结果为QJCY,预测正确
./test_1004/541-8XHJ.png的预测结果为8XHJ,预测正确
./test_1004/542-QH7P.png的预测结果为QH7P,预测正确
./test_1004/543-OLH6.png的预测结果为OLH6,预测正确
./test_1004/544-R6JD.png的预测结果为R6JD,预测正确
./test_1004/545-VQOE.png的预测结果为VQOE,预测正确
./test_1004/546-VR75.png的预测结果为VR75,预测正确
./test_1004/547-7H87.png的预测结果为7H87,预测正确
./test_1004/548-D4Y3.png的预测结果为D4Y3,预测正确
./test_1004/549-DJ4Y.png的预测结果为DJ4Y,预测正确
./test_1004/55-FW6E.png的预测结果为FW6E,预测正确
./test_1004/550-C5O2.png的预测结果为C5O2,预测正确
./test_1004/551-YUAQ.png的预测结果为YUAQ,预测正确
./test_1004/552-4EFC.png的预测结果为4EFC,预测正确
./test_1004/553-TSEL.png的预测结果为TSEL,预测正确
./test_1004/554-CPNL.png的预测结果为CPNL,预测正确
./test_1004/666666-G58X.png的预测结果为G58X,预测正确
./test_1004/556-ICGM.png的预测结果为ICGM,预测正确
./test_1004/557-JLX9.png的预测结果为JLX9,预测正确
./test_1004/558-9NT6.png的预测结果为9NT6,预测正确
./test_1004/559-3S62.png的预测结果为3S62,预测正确
./test_1004/56-QLE0.png的预测结果为QLE0,预测正确
./test_1004/560-ZZYF.png的预测结果为ZZYF,预测正确
./test_1004/561-UKEK.png的预测结果为UKEK,预测正确
./test_1004/562-J6D8.png的预测结果为J6D8,预测正确
./test_1004/563-YKKN.png的预测结果为YKKN,预测正确
./test_1004/564-398Z.png的预测结果为398Z,预测正确
./test_1004/565-RLOG.png的预测结果为RLOG,预测正确
./test_1004/566-PKBX.png的预测结果为PKBX,预测正确
./test_1004/567-RLO4.png的预测结果为RLO4,预测正确
./test_1004/568-8T79.png的预测结果为8T79,预测正确
./test_1004/569-0U1C.png的预测结果为0U1C,预测正确
./test_1004/57-6MNT.png的预测结果为6MNT,预测正确
./test_1004/570-AGZC.png的预测结果为AGZC,预测正确
./test_1004/571-54NL.png的预测结果为54NL,预测正确
./test_1004/572-IJJ4.png的预测结果为IJJ4,预测正确
./test_1004/573-0LBP.png的预测结果为OLBP,预测错误
./test_1004/574-GEDC.png的预测结果为GEDC,预测正确
./test_1004/575-C0C5.png的预测结果为C0C5,预测正确
./test_1004/576-URUV.png的预测结果为URUV,预测正确
./test_1004/577-4R8L.png的预测结果为4R8L,预测正确
./test_1004/578-S4A7.png的预测结果为S4A7,预测正确
./test_1004/579-OMC6.png的预测结果为OMC6,预测正确
./test_1004/58-70CJ.png的预测结果为70CJ,预测正确
./test_1004/580-JFTU.png的预测结果为JFTU,预测正确
./test_1004/581-6SLP.png的预测结果为6SLP,预测正确
./test_1004/582-B9PI.png的预测结果为B9PT,预测错误
./test_1004/583-IB57.png的预测结果为IB57,预测正确
./test_1004/584-Z14U.png的预测结果为Z14U,预测正确
./test_1004/585-IUHO.png的预测结果为IUHO,预测正确
./test_1004/586-CR2Q.png的预测结果为CR2Q,预测正确
./test_1004/587-7MX2.png的预测结果为7MX2,预测正确
./test_1004/588-D0EI.png的预测结果为D0EI,预测正确
./test_1004/589-JU80.png的预测结果为JU80,预测正确
./test_1004/59-9B8E.png的预测结果为9B8E,预测正确
./test_1004/590-D5H6.png的预测结果为D5H6,预测正确
./test_1004/591-WNHA.png的预测结果为WNHA,预测正确
./test_1004/592-QFT1.png的预测结果为QFT1,预测正确
./test_1004/593-88G6.png的预测结果为88G6,预测正确
./test_1004/594-7LX6.png的预测结果为7LX6,预测正确
./test_1004/595-ZDQN.png的预测结果为ZDQN,预测正确
./test_1004/596-FFYU.png的预测结果为FFYU,预测正确
./test_1004/597-GANI.png的预测结果为GANI,预测正确
./test_1004/598-OOBZ.png的预测结果为OOBZ,预测正确
./test_1004/599-LYNB.png的预测结果为LYNB,预测正确
./test_1004/6-K1YZ.png的预测结果为K1YZ,预测正确
./test_1004/60-3Y05.png的预测结果为3Y05,预测正确
./test_1004/600-TDN8.png的预测结果为TDN8,预测正确
./test_1004/601-BZ07.png的预测结果为BZ07,预测正确
./test_1004/602-75P4.png的预测结果为75P4,预测正确
./test_1004/603-YN1F.png的预测结果为YN1F,预测正确
./test_1004/604-04OG.png的预测结果为04OG,预测正确
./test_1004/605-9TCT.png的预测结果为9TCT,预测正确
./test_1004/606-6R24.png的预测结果为6R24,预测正确
./test_1004/607-9IX4.png的预测结果为9IX4,预测正确
./test_1004/608-L6KL.png的预测结果为L6KL,预测正确
./test_1004/609-LKYB.png的预测结果为LKYB,预测正确
./test_1004/61-U579.png的预测结果为U579,预测正确
./test_1004/610-6RKN.png的预测结果为6RKN,预测正确
./test_1004/611-NY5E.png的预测结果为NY5E,预测正确
./test_1004/612-GD95.png的预测结果为GD95,预测正确
./test_1004/613-AN4J.png的预测结果为AN4J,预测正确
./test_1004/614-VJMH.png的预测结果为VJMH,预测正确
./test_1004/615-1QA0.png的预测结果为1QA0,预测正确
./test_1004/616-T9XH.png的预测结果为T9XH,预测正确
./test_1004/617-9S6K.png的预测结果为9S6K,预测正确
./test_1004/618-RPFZ.png的预测结果为RPFZ,预测正确
./test_1004/619-TQ29.png的预测结果为TQ29,预测正确
./test_1004/62-A1UB.png的预测结果为A1UB,预测正确
./test_1004/620-E9BI.png的预测结果为E9BI,预测正确
./test_1004/621-6BYN.png的预测结果为6BYN,预测正确
./test_1004/622-CJ0T.png的预测结果为CJOT,预测错误
./test_1004/623-NVQT.png的预测结果为NVQT,预测正确
./test_1004/624-EQU6.png的预测结果为EQU6,预测正确
./test_1004/625-M998.png的预测结果为M998,预测正确
./test_1004/626-EPVO.png的预测结果为EPVO,预测正确
./test_1004/627-U03Q.png的预测结果为U03Q,预测正确
./test_1004/628-3Y6R.png的预测结果为3Y6R,预测正确
./test_1004/629-7R3E.png的预测结果为7R3E,预测正确
./test_1004/63-0FMT.png的预测结果为0FMT,预测正确
./test_1004/630-7FQ5.png的预测结果为7FQ5,预测正确
./test_1004/631-7E9F.png的预测结果为7E9F,预测正确
./test_1004/632-LR9V.png的预测结果为LR9V,预测正确
./test_1004/633-1T01.png的预测结果为1TO1,预测错误
./test_1004/634-D5ED.png的预测结果为D5ED,预测正确
./test_1004/635-5WM9.png的预测结果为5WM9,预测正确
./test_1004/636-QWVQ.png的预测结果为QWVQ,预测正确
./test_1004/637-P2B0.png的预测结果为P2B0,预测正确
./test_1004/638-83ZP.png的预测结果为83ZP,预测正确
./test_1004/639-D1SV.png的预测结果为D1SV,预测正确
./test_1004/64-CGXO.png的预测结果为CGXO,预测正确
./test_1004/640-WENY.png的预测结果为WENY,预测正确
./test_1004/641-N9RC.png的预测结果为N9RC,预测正确
./test_1004/642-3ECQ.png的预测结果为3ECQ,预测正确
./test_1004/643-O4DW.png的预测结果为O4DW,预测正确
./test_1004/644-05CZ.png的预测结果为05CZ,预测正确
./test_1004/645-5FPA.png的预测结果为5FPA,预测正确
./test_1004/646-GUBW.png的预测结果为GUBW,预测正确
./test_1004/647-CUT2.png的预测结果为CUT2,预测正确
./test_1004/648-EEGR.png的预测结果为EEGR,预测正确
./test_1004/649-R911.png的预测结果为R911,预测正确
./test_1004/65-KFDH.png的预测结果为KFDH,预测正确
./test_1004/650-QSQC.png的预测结果为QSQC,预测正确
./test_1004/651-K1OE.png的预测结果为K1OE,预测正确
./test_1004/652-EYJN.png的预测结果为EYJN,预测正确
./test_1004/653-BWL2.png的预测结果为BWL2,预测正确
./test_1004/654-GZTU.png的预测结果为GZTU,预测正确
./test_1004/655-YC5L.png的预测结果为YC5L,预测正确
./test_1004/656-N2U8.png的预测结果为N2U8,预测正确
./test_1004/657-NXT0.png的预测结果为NXT0,预测正确
./test_1004/658-03HA.png的预测结果为03HA,预测正确
./test_1004/659-ZT3A.png的预测结果为ZT3A,预测正确
./test_1004/66-DWTQ.png的预测结果为DWTQ,预测正确
./test_1004/660-MQT1.png的预测结果为MQT1,预测正确
./test_1004/661-RYXG.png的预测结果为RYXG,预测正确
./test_1004/662-CSNM.png的预测结果为CSNM,预测正确
./test_1004/663-4LDV.png的预测结果为4LDV,预测正确
./test_1004/664-MY7Z.png的预测结果为MY7Z,预测正确
./test_1004/665-RPF9.png的预测结果为RPF9,预测正确
./test_1004/666-AWFN.png的预测结果为AWFN,预测正确
./test_1004/667-679V.png的预测结果为679V,预测正确
./test_1004/668-Q2C3.png的预测结果为Q2C3,预测正确
./test_1004/669-9JDU.png的预测结果为9JDU,预测正确
./test_1004/67-338M.png的预测结果为338M,预测正确
./test_1004/670-4ODK.png的预测结果为4ODK,预测正确
./test_1004/671-MOCH.png的预测结果为MOCH,预测正确
./test_1004/672-WYBN.png的预测结果为WYBN,预测正确
./test_1004/673-XCAS.png的预测结果为XCAS,预测正确
./test_1004/674-OAN9.png的预测结果为OAN9,预测正确
./test_1004/675-9J0R.png的预测结果为9J0R,预测正确
./test_1004/676-7E6B.png的预测结果为7E6B,预测正确
./test_1004/677-LFQF.png的预测结果为LFQF,预测正确
./test_1004/678-9BU4.png的预测结果为9BU4,预测正确
./test_1004/679-W5V7.png的预测结果为W5V7,预测正确
./test_1004/68-2THW.png的预测结果为2THW,预测正确
./test_1004/680-4KZ3.png的预测结果为4KZ3,预测正确
./test_1004/681-AB09.png的预测结果为AB09,预测正确
./test_1004/682-JDRH.png的预测结果为JDRH,预测正确
./test_1004/683-46R6.png的预测结果为46R6,预测正确
./test_1004/684-43LV.png的预测结果为43LV,预测正确
./test_1004/685-2543.png的预测结果为2543,预测正确
./test_1004/686-NNPP.png的预测结果为NNPP,预测正确
./test_1004/687-ZR1O.png的预测结果为ZR1O,预测正确
./test_1004/688-H5ZC.png的预测结果为H5ZC,预测正确
./test_1004/689-B008.png的预测结果为B0O8,预测错误
./test_1004/69-91OV.png的预测结果为91OV,预测正确
./test_1004/690-QS11.png的预测结果为QS11,预测正确
./test_1004/691-3CDJ.png的预测结果为3CDJ,预测正确
./test_1004/692-DEHN.png的预测结果为DEHN,预测正确
./test_1004/693-GJQK.png的预测结果为GJQK,预测正确
./test_1004/694-77AF.png的预测结果为71AF,预测错误
./test_1004/695-GLZ9.png的预测结果为GLZ9,预测正确
./test_1004/696-RLKJ.png的预测结果为RLKJ,预测正确
./test_1004/697-B9CJ.png的预测结果为B9CJ,预测正确
./test_1004/698-F0OQ.png的预测结果为F0OQ,预测正确
./test_1004/699-3ALE.png的预测结果为3ALE,预测正确
./test_1004/7-1HZ8.png的预测结果为1HZ8,预测正确
./test_1004/70-AD0Y.png的预测结果为AD0Y,预测正确
./test_1004/700-U5IV.png的预测结果为U5IV,预测正确
./test_1004/701-WMMU.png的预测结果为WMMU,预测正确
./test_1004/702-GHLD.png的预测结果为GHLD,预测正确
./test_1004/703-REMJ.png的预测结果为REMJ,预测正确
./test_1004/704-AE0A.png的预测结果为AE0A,预测正确
./test_1004/705-CR8Z.png的预测结果为CR8Z,预测正确
./test_1004/706-AIWR.png的预测结果为AIWR,预测正确
./test_1004/707-DOM0.png的预测结果为DOM0,预测正确
./test_1004/708-EOQ0.png的预测结果为EOQO,预测错误
./test_1004/709-2KX2.png的预测结果为2KX2,预测正确
./test_1004/71-MBHP.png的预测结果为MBHP,预测正确
./test_1004/710-0EXN.png的预测结果为0EXN,预测正确
./test_1004/711-G9MQ.png的预测结果为G9MQ,预测正确
./test_1004/712-4TER.png的预测结果为4TER,预测正确
./test_1004/713-DPCM.png的预测结果为DPCM,预测正确
./test_1004/714-KS07.png的预测结果为KSO7,预测错误
./test_1004/715-L6B0.png的预测结果为L6B0,预测正确
./test_1004/716-1Z88.png的预测结果为1Z88,预测正确
./test_1004/717-71JK.png的预测结果为711K,预测错误
./test_1004/718-2E2V.png的预测结果为2E2V,预测正确
./test_1004/719-IJX2.png的预测结果为IJX2,预测正确
./test_1004/72-2WYQ.png的预测结果为2WYQ,预测正确
./test_1004/720-DJPY.png的预测结果为DDPY,预测错误
./test_1004/721-ETM2.png的预测结果为ETM2,预测正确
./test_1004/722-1J4M.png的预测结果为1J4M,预测正确
./test_1004/723-8AP3.png的预测结果为8AP3,预测正确
./test_1004/724-JRXK.png的预测结果为JRXK,预测正确
./test_1004/725-BAVE.png的预测结果为BAVE,预测正确
./test_1004/726-X7C9.png的预测结果为X7C9,预测正确
./test_1004/727-0V15.png的预测结果为0V16,预测错误
./test_1004/728-UAXU.png的预测结果为UAXU,预测正确
./test_1004/729-GD9U.png的预测结果为GD9U,预测正确
./test_1004/73-LGYY.png的预测结果为LGYY,预测正确
./test_1004/730-G8L2.png的预测结果为G8L2,预测正确
./test_1004/731-AFU9.png的预测结果为AFU9,预测正确
./test_1004/732-CBS7.png的预测结果为CBS7,预测正确
./test_1004/733-6ZVP.png的预测结果为6ZVP,预测正确
./test_1004/734-WUWN.png的预测结果为WUWN,预测正确
./test_1004/735-BQU1.png的预测结果为BQU1,预测正确
./test_1004/736-VKGL.png的预测结果为VKKG,预测错误
./test_1004/737-6DYR.png的预测结果为6DYR,预测正确
./test_1004/738-HVX2.png的预测结果为HVX2,预测正确
./test_1004/739-T3MS.png的预测结果为T3MS,预测正确
./test_1004/74-VN5B.png的预测结果为VN5B,预测正确
./test_1004/740-IRR8.png的预测结果为IRR8,预测正确
./test_1004/741-GJUP.png的预测结果为GJUP,预测正确
./test_1004/742-Q2JI.png的预测结果为Q2JI,预测正确
./test_1004/743-5ZH9.png的预测结果为5ZH9,预测正确
./test_1004/744-DA39.png的预测结果为DA39,预测正确
./test_1004/745-DMXX.png的预测结果为DMXX,预测正确
./test_1004/746-EPAT.png的预测结果为EPAT,预测正确
./test_1004/747-4S0M.png的预测结果为4S0M,预测正确
./test_1004/748-FR0W.png的预测结果为FROW,预测错误
./test_1004/749-NSI8.png的预测结果为NSI8,预测正确
./test_1004/75-2OHU.png的预测结果为2OHU,预测正确
./test_1004/750-YA2N.png的预测结果为YANN,预测错误
./test_1004/751-50U5.png的预测结果为50U6,预测错误
./test_1004/752-KSCD.png的预测结果为KSCD,预测正确
./test_1004/753-PFP0.png的预测结果为PFPO,预测错误
./test_1004/754-JQT8.png的预测结果为JQT8,预测正确
./test_1004/755-J71O.png的预测结果为J71O,预测正确
./test_1004/756-6Y0J.png的预测结果为6YOJ,预测错误
./test_1004/757-UJS9.png的预测结果为UJS9,预测正确
./test_1004/758-35NF.png的预测结果为35NF,预测正确
./test_1004/759-O9LL.png的预测结果为O9LL,预测正确
./test_1004/76-TKOS.png的预测结果为TKOS,预测正确
./test_1004/760-2X61.png的预测结果为2X61,预测正确
./test_1004/761-80S7.png的预测结果为8OS7,预测错误
./test_1004/762-EKE0.png的预测结果为EKE0,预测正确
./test_1004/763-27JY.png的预测结果为27JY,预测正确
./test_1004/764-QI3Q.png的预测结果为QI3Q,预测正确
./test_1004/765-8BZ6.png的预测结果为8BZ6,预测正确
./test_1004/766-BG3M.png的预测结果为BG3M,预测正确
./test_1004/767-MBCU.png的预测结果为MBCU,预测正确
./test_1004/768-ATX7.png的预测结果为ATX7,预测正确
./test_1004/769-V898.png的预测结果为V898,预测正确
./test_1004/77-Z3CC.png的预测结果为Z3CC,预测正确
./test_1004/770-MLAZ.png的预测结果为MLAZ,预测正确
./test_1004/771-L3RE.png的预测结果为L3RE,预测正确
./test_1004/772-LL23.png的预测结果为LL23,预测正确
./test_1004/773-I6IP.png的预测结果为I6IP,预测正确
./test_1004/774-5RWB.png的预测结果为5RWB,预测正确
./test_1004/775-D9Z9.png的预测结果为D9Z9,预测正确
./test_1004/776-6XQA.png的预测结果为6XQA,预测正确
./test_1004/777-UESN.png的预测结果为UESN,预测正确
./test_1004/778-ME47.png的预测结果为OEN7,预测错误
./test_1004/779-4MW1.png的预测结果为4MW1,预测正确
./test_1004/78-Z93D.png的预测结果为Z93D,预测正确
./test_1004/780-4I8K.png的预测结果为4I8K,预测正确
./test_1004/781-EZK2.png的预测结果为EZK2,预测正确
./test_1004/782-FNAQ.png的预测结果为FNAQ,预测正确
./test_1004/783-ZUK6.png的预测结果为ZUK6,预测正确
./test_1004/784-T4DL.png的预测结果为T4DL,预测正确
./test_1004/785-ZR3S.png的预测结果为ZR3S,预测正确
./test_1004/786-ZYIM.png的预测结果为ZYIM,预测正确
./test_1004/787-M0IB.png的预测结果为M0IB,预测正确
./test_1004/788-ANWK.png的预测结果为ANWK,预测正确
./test_1004/789-GTP2.png的预测结果为GTP2,预测正确
./test_1004/79-FNDC.png的预测结果为FN0C,预测错误
./test_1004/790-R2DY.png的预测结果为R2DY,预测正确
./test_1004/791-BCJM.png的预测结果为BCJM,预测正确
./test_1004/792-450E.png的预测结果为450E,预测正确
./test_1004/793-IQ15.png的预测结果为IQ15,预测正确
./test_1004/794-CHCT.png的预测结果为CHCT,预测正确
./test_1004/795-PDBG.png的预测结果为PDBG,预测正确
./test_1004/796-51TS.png的预测结果为51TS,预测正确
./test_1004/797-0QWA.png的预测结果为0QWA,预测正确
./test_1004/798-33PC.png的预测结果为33PC,预测正确
./test_1004/799-5ZAW.png的预测结果为5ZAW,预测正确
./test_1004/8-2586.png的预测结果为2586,预测正确
./test_1004/80-FT9L.png的预测结果为FT9L,预测正确
./test_1004/800-UV0T.png的预测结果为UV0T,预测正确
./test_1004/801-BVT4.png的预测结果为BVT4,预测正确
./test_1004/802-E9RV.png的预测结果为E9RV,预测正确
./test_1004/803-9A79.png的预测结果为9A79,预测正确
./test_1004/804-JEID.png的预测结果为JEID,预测正确
./test_1004/805-IZ3U.png的预测结果为IZ3U,预测正确
./test_1004/806-K1YP.png的预测结果为K1YP,预测正确
./test_1004/807-O8AR.png的预测结果为O8AR,预测正确
./test_1004/808-AXHA.png的预测结果为AXHA,预测正确
./test_1004/809-13F6.png的预测结果为13F6,预测正确
./test_1004/81-9AXW.png的预测结果为9AXW,预测正确
./test_1004/810-9N4H.png的预测结果为9N4H,预测正确
./test_1004/811-IG4M.png的预测结果为IG4M,预测正确
./test_1004/812-M1MS.png的预测结果为M1MS,预测正确
./test_1004/813-0700.png的预测结果为0700,预测正确
./test_1004/814-5HDI.png的预测结果为5HDI,预测正确
./test_1004/815-7WJ9.png的预测结果为7WJ9,预测正确
./test_1004/816-RVB9.png的预测结果为RVB9,预测正确
./test_1004/817-KM73.png的预测结果为KM73,预测正确
./test_1004/818-AFSV.png的预测结果为AFSV,预测正确
./test_1004/819-V3AO.png的预测结果为VAAO,预测错误
./test_1004/82-JJJX.png的预测结果为JJJX,预测正确
./test_1004/820-H4S7.png的预测结果为H4S7,预测正确
./test_1004/821-5641.png的预测结果为5611,预测错误
./test_1004/822-J6EM.png的预测结果为J6EM,预测正确
./test_1004/823-ZT1D.png的预测结果为ZT1D,预测正确
./test_1004/824-X8XZ.png的预测结果为X8XZ,预测正确
./test_1004/825-3QMW.png的预测结果为3QMW,预测正确
./test_1004/826-8EFN.png的预测结果为8EFN,预测正确
./test_1004/827-Q63F.png的预测结果为Q63F,预测正确
./test_1004/828-TXCY.png的预测结果为TKCY,预测错误
./test_1004/829-GQIS.png的预测结果为GQIS,预测正确
./test_1004/83-CN5Z.png的预测结果为CN5Z,预测正确
./test_1004/830-3GLU.png的预测结果为3GLU,预测正确
./test_1004/831-EY3O.png的预测结果为EY3O,预测正确
./test_1004/832-6KFK.png的预测结果为6KFK,预测正确
./test_1004/833-GZW3.png的预测结果为GZW3,预测正确
./test_1004/834-4ADI.png的预测结果为4ADI,预测正确
./test_1004/835-8BHP.png的预测结果为88HP,预测错误
./test_1004/836-RS1S.png的预测结果为RS1S,预测正确
./test_1004/837-AIJH.png的预测结果为AIJH,预测正确
./test_1004/838-2SPH.png的预测结果为2SPH,预测正确
./test_1004/839-DT49.png的预测结果为DT49,预测正确
./test_1004/84-RVFS.png的预测结果为RVFS,预测正确
./test_1004/840-1PP4.png的预测结果为1PP4,预测正确
./test_1004/841-TCC0.png的预测结果为TCC0,预测正确
./test_1004/842-L0KA.png的预测结果为L0KA,预测正确
./test_1004/843-Y8BS.png的预测结果为Y8BS,预测正确
./test_1004/844-O7NK.png的预测结果为07NK,预测错误
./test_1004/845-JOC0.png的预测结果为JOC0,预测正确
./test_1004/846-P8HN.png的预测结果为P8HN,预测正确
./test_1004/847-NMRY.png的预测结果为NMRY,预测正确
./test_1004/848-TVB8.png的预测结果为TVB8,预测正确
./test_1004/849-W2WO.png的预测结果为W2WO,预测正确
./test_1004/85-RCYG.png的预测结果为RCYG,预测正确
./test_1004/850-15X2.png的预测结果为1XX2,预测错误
./test_1004/851-4MT9.png的预测结果为4MT9,预测正确
./test_1004/852-MZH7.png的预测结果为MZH7,预测正确
./test_1004/853-Q95P.png的预测结果为Q95P,预测正确
./test_1004/854-JPZO.png的预测结果为JPZO,预测正确
./test_1004/855-JCG5.png的预测结果为JCG5,预测正确
./test_1004/856-BKCA.png的预测结果为BKCA,预测正确
./test_1004/857-QEXL.png的预测结果为QEXL,预测正确
./test_1004/858-3G6M.png的预测结果为3G6M,预测正确
./test_1004/859-B4KP.png的预测结果为B4KP,预测正确
./test_1004/86-OVN1.png的预测结果为OVN1,预测正确
./test_1004/860-P93O.png的预测结果为P93O,预测正确
./test_1004/861-VO3A.png的预测结果为VO3A,预测正确
./test_1004/862-9W9W.png的预测结果为9W9W,预测正确
./test_1004/863-GHMV.png的预测结果为GHMV,预测正确
./test_1004/864-4W48.png的预测结果为4W48,预测正确
./test_1004/865-EMKF.png的预测结果为EMKF,预测正确
./test_1004/866-84IL.png的预测结果为84IL,预测正确
./test_1004/867-LPFE.png的预测结果为LPFE,预测正确
./test_1004/868-73HD.png的预测结果为73HD,预测正确
./test_1004/869-LO50.png的预测结果为LO5O,预测错误
./test_1004/87-JS8E.png的预测结果为JS8E,预测正确
./test_1004/870-JJL6.png的预测结果为JJL6,预测正确
./test_1004/871-K6OJ.png的预测结果为K6DJ,预测错误
./test_1004/872-QRSU.png的预测结果为QRSU,预测正确
./test_1004/873-B3FA.png的预测结果为B3FA,预测正确
./test_1004/874-JI5X.png的预测结果为JI5X,预测正确
./test_1004/875-FIW1.png的预测结果为FIW1,预测正确
./test_1004/876-Z648.png的预测结果为Z648,预测正确
./test_1004/877-I7T8.png的预测结果为I7T8,预测正确
./test_1004/878-WN3H.png的预测结果为WN3H,预测正确
./test_1004/879-RA2G.png的预测结果为RA2G,预测正确
./test_1004/88-24QM.png的预测结果为24QM,预测正确
./test_1004/880-RGKU.png的预测结果为RGKU,预测正确
./test_1004/881-0QJ3.png的预测结果为0QJ3,预测正确
./test_1004/882-089Y.png的预测结果为089Y,预测正确
./test_1004/883-391L.png的预测结果为391L,预测正确
./test_1004/884-UCNH.png的预测结果为UCNH,预测正确
./test_1004/885-LNBS.png的预测结果为LNBS,预测正确
./test_1004/886-G8BI.png的预测结果为G8BI,预测正确
./test_1004/887-UJG7.png的预测结果为UJG7,预测正确
./test_1004/888-NKN5.png的预测结果为NKN5,预测正确
./test_1004/889-3WGQ.png的预测结果为3WGQ,预测正确
./test_1004/89-U4AE.png的预测结果为U4AE,预测正确
./test_1004/890-AJCD.png的预测结果为AJCD,预测正确
./test_1004/891-DVZU.png的预测结果为DVZU,预测正确
./test_1004/892-87LD.png的预测结果为87LD,预测正确
./test_1004/893-JS38.png的预测结果为JS38,预测正确
./test_1004/894-73WT.png的预测结果为73WT,预测正确
./test_1004/895-A4U5.png的预测结果为A4U5,预测正确
./test_1004/896-WNVA.png的预测结果为WNNA,预测错误
./test_1004/897-N3PS.png的预测结果为N3PS,预测正确
./test_1004/898-3OYZ.png的预测结果为3OYZ,预测正确
./test_1004/899-CM01.png的预测结果为CM01,预测正确
./test_1004/9-84AI.png的预测结果为84AI,预测正确
./test_1004/90-VVFC.png的预测结果为VVFC,预测正确
./test_1004/900-G916.png的预测结果为G916,预测正确
./test_1004/901-HXRD.png的预测结果为HXRD,预测正确
./test_1004/902-20KV.png的预测结果为00KV,预测错误
./test_1004/903-RS1S.png的预测结果为RS1S,预测正确
./test_1004/904-05NS.png的预测结果为05NS,预测正确
./test_1004/905-QXOT.png的预测结果为QXOT,预测正确
./test_1004/906-55X1.png的预测结果为55X1,预测正确
./test_1004/907-6WST.png的预测结果为6WST,预测正确
./test_1004/908-V8NV.png的预测结果为V8NV,预测正确
./test_1004/909-S18F.png的预测结果为S18F,预测正确
./test_1004/91-LU42.png的预测结果为LU42,预测正确
./test_1004/910-UOOR.png的预测结果为UOOR,预测正确
./test_1004/911-J2VQ.png的预测结果为J2VQ,预测正确
./test_1004/912-3FUJ.png的预测结果为3FUJ,预测正确
./test_1004/913-HVJO.png的预测结果为HVJO,预测正确
./test_1004/914-21EV.png的预测结果为21EV,预测正确
./test_1004/915-2ASZ.png的预测结果为2ASZ,预测正确
./test_1004/916-80QF.png的预测结果为80QF,预测正确
./test_1004/917-GEDD.png的预测结果为GEDD,预测正确
./test_1004/918-TSYZ.png的预测结果为TSYZ,预测正确
./test_1004/919-JDO8.png的预测结果为JDO8,预测正确
./test_1004/92-71BO.png的预测结果为71BO,预测正确
./test_1004/920-M5Z1.png的预测结果为M6Z1,预测错误
./test_1004/921-SHL1.png的预测结果为SHL1,预测正确
./test_1004/922-891E.png的预测结果为891E,预测正确
./test_1004/923-RHBD.png的预测结果为RHBD,预测正确
./test_1004/924-IG1H.png的预测结果为IG1H,预测正确
./test_1004/925-1S17.png的预测结果为1S17,预测正确
./test_1004/926-EPBJ.png的预测结果为EPPJ,预测错误
./test_1004/927-N0QH.png的预测结果为N0QH,预测正确
./test_1004/928-K3P2.png的预测结果为K3P2,预测正确
./test_1004/929-N6ZX.png的预测结果为N6ZX,预测正确
./test_1004/93-OQ1O.png的预测结果为OQ1O,预测正确
./test_1004/930-ZMFL.png的预测结果为ZMFL,预测正确
./test_1004/931-ORMU.png的预测结果为ORMU,预测正确
./test_1004/932-TLM0.png的预测结果为TLM0,预测正确
./test_1004/933-R8J1.png的预测结果为R8J1,预测正确
./test_1004/934-KPSU.png的预测结果为KPSU,预测正确
./test_1004/935-6DIL.png的预测结果为6ODL,预测错误
./test_1004/936-E8ER.png的预测结果为E8ER,预测正确
./test_1004/937-3LFV.png的预测结果为3LFV,预测正确
./test_1004/938-RVWL.png的预测结果为RVWL,预测正确
./test_1004/939-QGWW.png的预测结果为QGWW,预测正确
./test_1004/94-YGCJ.png的预测结果为YGCJ,预测正确
./test_1004/940-LTWQ.png的预测结果为LTWQ,预测正确
./test_1004/941-XRQQ.png的预测结果为XRQQ,预测正确
./test_1004/942-OFIL.png的预测结果为OFIL,预测正确
./test_1004/943-7HP1.png的预测结果为7HP1,预测正确
./test_1004/944-OVEG.png的预测结果为OVEG,预测正确
./test_1004/945-ERKR.png的预测结果为ERKR,预测正确
./test_1004/946-W236.png的预测结果为W236,预测正确
./test_1004/947-141S.png的预测结果为141S,预测正确
./test_1004/948-85SG.png的预测结果为85SG,预测正确
./test_1004/949-KDA6.png的预测结果为KDA6,预测正确
./test_1004/95-Z3HI.png的预测结果为Z3HI,预测正确
./test_1004/950-8DL0.png的预测结果为8DL0,预测正确
./test_1004/951-0V2W.png的预测结果为OV2W,预测错误
./test_1004/952-7OSH.png的预测结果为7OSH,预测正确
./test_1004/953-LIEK.png的预测结果为LIEK,预测正确
./test_1004/954-Z1CK.png的预测结果为Z1CK,预测正确
./test_1004/955-CDIE.png的预测结果为CDIE,预测正确
./test_1004/956-5LW9.png的预测结果为5LW9,预测正确
./test_1004/957-BJYH.png的预测结果为BJYH,预测正确
./test_1004/958-E7N1.png的预测结果为E7N1,预测正确
./test_1004/959-CRTK.png的预测结果为CRTK,预测正确
./test_1004/96-KM2E.png的预测结果为KM2E,预测正确
./test_1004/960-NCJZ.png的预测结果为NCJZ,预测正确
./test_1004/961-YHRA.png的预测结果为YHRA,预测正确
./test_1004/962-TQR9.png的预测结果为TQR9,预测正确
./test_1004/963-O2G7.png的预测结果为O2G7,预测正确
./test_1004/964-V19S.png的预测结果为V19S,预测正确
./test_1004/965-3CHJ.png的预测结果为3CHJ,预测正确
./test_1004/966-GPX7.png的预测结果为GPX7,预测正确
./test_1004/967-N1Z2.png的预测结果为N172,预测错误
./test_1004/968-I620.png的预测结果为I620,预测正确
./test_1004/969-95KI.png的预测结果为95KI,预测正确
./test_1004/97-NUME.png的预测结果为NUME,预测正确
./test_1004/970-22U3.png的预测结果为22U3,预测正确
./test_1004/971-MTVV.png的预测结果为MTVV,预测正确
./test_1004/972-18EZ.png的预测结果为18EZ,预测正确
./test_1004/973-P9UQ.png的预测结果为P9UQ,预测正确
./test_1004/974-0EQH.png的预测结果为0EQH,预测正确
./test_1004/975-5NFH.png的预测结果为5NFH,预测正确
./test_1004/976-1O27.png的预测结果为1O27,预测正确
./test_1004/977-S139.png的预测结果为S139,预测正确
./test_1004/978-XD25.png的预测结果为XD25,预测正确
./test_1004/979-SJVQ.png的预测结果为SJVQ,预测正确
./test_1004/98-9PT9.png的预测结果为9PT9,预测正确
./test_1004/980-2WH9.png的预测结果为2WH9,预测正确
./test_1004/981-C6T5.png的预测结果为C6T5,预测正确
./test_1004/982-P7Z4.png的预测结果为PZZ4,预测错误
./test_1004/983-WRUZ.png的预测结果为WRUZ,预测正确
./test_1004/984-IMQS.png的预测结果为IMQS,预测正确
./test_1004/985-JN0K.png的预测结果为JN0K,预测正确
./test_1004/986-1INH.png的预测结果为1INH,预测正确
./test_1004/987-HL36.png的预测结果为HL36,预测正确
./test_1004/988-CUW6.png的预测结果为CUW6,预测正确
./test_1004/989-2CS7.png的预测结果为2CS7,预测正确
./test_1004/99-U36U.png的预测结果为U36U,预测正确
./test_1004/990-UX5D.png的预测结果为UX5D,预测正确
./test_1004/991-5H12.png的预测结果为5H12,预测正确
./test_1004/992-BY4S.png的预测结果为BY4S,预测正确
./test_1004/993-O4IP.png的预测结果为O4IP,预测正确
./test_1004/994-0MTO.png的预测结果为0MTO,预测正确
./test_1004/995-HJTH.png的预测结果为HJTH,预测正确
./test_1004/996-C1G6.png的预测结果为C1G6,预测正确
./test_1004/997-V676.png的预测结果为V676,预测正确
./test_1004/998-DMKK.png的预测结果为OMKK,预测错误
./test_1004/999-S93X.png的预测结果为S93X,预测正确
92.80%
五、总结

这里我们只是对图像进行了灰度处理,并没有进一步的优化图像。如果要进一步提高识别精度,还可以增加训练数据集和训练周期,或者优化下神经网络各层结构。

本文只是实战操作,如果有同样需求的朋友可以更快上手,PyTorch里面还很多值得学习的东西,感兴趣的可以自己去研究模型的推理,以及源码。
如果想要代码文件请在评论区留言。

Canvas图形编辑器-数据结构与History(undo/redo)

这是作为
社区老给我推Canvas,于是我也学习Canvas做了个简历编辑器
的后续内容,主要是介绍了对数据结构的设计以及
History
能力的实现。

关于
Canvas
简历编辑器项目的相关文章:

描述

对于编辑器而言,
History
也就是
undo

redo
是必不可少的能力,实现历史记录的方法通常有两种:

  1. 存储全量快照,也就是说我我们每进行一个操作,都需要将全量的数据通常也就是
    JSON
    格式的数据存到一个数组里,如果用户此时触发了
    redo
    就将全量的数据取出应用到
    Editor
    对象当中。这种实现方式的优点是简单,不需要过多的设计,缺点就是一旦操作的多了就容易炸内存。

  2. 基于
    Op
    的实现,
    Op
    就是对于一个操作的原子化记录,举个例子如果将图形
    A
    向右移动
    3px
    ,那么这个
    Op
    就可以是
    type: "MOVE", offset: [3, 0]
    ,那么如果想要做回退操作依然很简单,只需要将其反向操作即
    type: "MOVE", offset: [-3, 0]
    就可以了,这种方式的优点是粒度更细,存储压力小,缺点是需要复杂的设计以及计算。

既然我们是从零开始设计一个编辑器,那么大概率是不会采用方案
1
的,我们更希望能够设计原子化的
Op
来实现
History
,所以从这个方向开始我们就需要先设计数据结构。

数据结构

我特别推荐大家去看一下
quill-delta
的数据结构设计,这个数据结构的设计非常棒,其可以用来描述一篇富文本,同时也可以用来构建
change
对富文本做完整的增删改操作,对于数据的
compose

invert

diff
等操作也一应俱全,而且
quill-delta
也可以是富文本
OT
协同算法的实现,这其中的设计还是非常牛逼的。

其实我之前也没有设计过数据结构,更不用谈设计
Op
去实现历史记录功能了,所以我在设计数据结构的时候是抓耳挠腮、寝食难安,想设计出
quill-delta
这种级别的数据描述几乎是不可能了,所以只能依照我的想法来简单地设计,这其中有很多不完善的地方后边可能还会有所改动。

因为之前也没有接触过
Canvas
,所以我的主要目标是学习,所以我希望任何的实现都以尽可能简单的方向走。那么在这里我认为任何元素都是矩形,因为绘制矩阵是比较简单的,所以图形元素基类的
x, y, width, height
属性是确定的,再加上还有层级结构,那么就再加一个
z
,此外由于需要标识图形,所以还需要给其设置一个
id

class Delta {
  public readonly id: string;
  protected x: number;
  protected y: number;
  protected z: number;
  protected width: number;
  protected height: number;
}

因为我想做一个插件化的实现,也就是说所有的图形都应该继承这个类,那么这个自定义的函数体肯定是需要存储自己的数据,所以在这里加一个
attrs
属性,又因为想简单实现整个功能,所以这个数据类型就被定义为
Record<string, string>
。因为是插件化的,每个图形的绘制应该由子类来实现,所以需要定义绘制函数的抽象方法,于是一个数据结构就这么设计好了,关于插件化的设计我们后续可以再继续聊。

abstract class Delta {
  public readonly id: string;
  protected x: number;
  protected y: number;
  protected z: number;
  protected width: number;
  protected height: number;
  public attrs: DeltaAttributes;
  public abstract drawing: (ctx: CanvasRenderingContext2D) => void;
}

那么现在已经有了基本的数据结构,我们可以设想一下究竟应该有哪几种操作,经过考虑大概无非是 插入
INSERT
、删除
DELETE
、移动
MOVE
、调整大小
RESIZE
、修改属性
REVISE
,这五个
Op
就可以覆盖我们对于当前编辑器图形的所有操作了,所以我们后续的设计都要围绕着这五个操作来进行。

看起来其实并不难,但实际上想要将其设计好并不容易,因为我们目标是
History
所以我们不光要顾及正向的操作,还需要设计好
invert
也就是反向操作,依旧以之前的
MOVE
操作举例,我们移动一个元素可以使用
MOVE(3, 0)
,反向操作就可以直接生成也就是
MOVE(3, 0).invert = MOVE(-3, 0)
,那么
RESIZE
操作呢,尤其是在多选操作时的
RESIZE
,我们需要想办法让其能够实现
invert
操作,一种方法是记录每个点的移动距离,但是这样对于每个
Op
存储的信息有点过多,我们在构造一个正向的
Op
时也需要将相关的数据拉到
Op
中,同样对于
REVISE
而言我们需要将属性的前值和后值都放在
Op
中才可以继续执行。

那么如何比较好的解决这个问题呢,很明显如果我们想用轻量的数据来承载内容,那么先前的数据在不一定会使用的情况下我们是没必要存储的,那是不是可以自动提取相关的内容作为
invert-op
呢,当然是可以的,我们可以在进行
invert
的时候,将未操作前的
Delta
一并作为参数传入就好了,我们可以来验证一下,我们的函数签名将会是
Op.invert(Delta) = Op'

// Prev DeltaSet
[{id: "xxx", x: x1, y: y1, width: w1, height: h1}]
// ResizeOp
RESIZE({id: "xxx", x: x2, y: y2})
// Next DeltaSet
[{id: "xxx", x: x1 + x2, y: y1 + y2, width: w1, height: w1}]
// Invert InsertOp
RESIZE({id: "xxx", x: -x2, y: -y2})

// Prev DeltaSet
[{id: "xxx", x: x1, y: y1, width: w1, height: h1}]
// ResizeOp
RESIZE({id: "xxx", x: x2, y: y2, width: w2, height: h2})
// Next DeltaSet
[{id: "xxx", x: x2, y: y2, width: w2, height: h2}]
// Invert InsertOp
RESIZE({id: "xxx", x: x1, y: y1, width: w1, height: h1})

看起来是没有问题的,所以我们现在可以设计全量的
Op

Invert
方法了,在这里因为我最开始是预计要设计组合也就是将几个图形组合在一起操作的能力,所以还预留了一个
parentId
作为后期开发拓展用,但是暂时是用不上的所以这个字段暂时可以忽略。下面的
Invert
实际上就是
case by case
地进行转换,
INSERT -> DELETE

DELETE -> INSERT

MOVE -> MOVE

RESIZE -> RESIZE

REVISE -> REVISE
。这其中的
DeltaSet
可以理解为当前的所有
Delta
数据,类型签名类似于
Record<string, Delta>
,是扁平的结构,便于数据查找。

export type OpPayload = {
  [OP_TYPE.INSERT]: { delta: Delta; parentId: string };
  [OP_TYPE.DELETE]: { id: string; parentId: string };
  [OP_TYPE.MOVE]: { ids: string[]; x: number; y: number };
  [OP_TYPE.RESIZE]: { id: string; x: number; y: number; width: number; height: number };
  [OP_TYPE.REVISE]: { id: string; attrs: DeltaAttributes };
};

export class Op<T extends OpType> {
  public readonly type: T;
  public readonly payload: OpPayload[T];
  constructor(type: T, payload: OpPayload[T]) {
    this.type = type;
    this.payload = payload;
  }

  public invert(prev: DeltaSet) {
    switch (this.type) {
      case OP_TYPE.INSERT: {
        const payload = this.payload as OpPayload[typeof OP_TYPE.INSERT];
        const { delta, parentId } = payload;
        return new Op(OP_TYPE.DELETE, { id: delta.id, parentId });
      }
      case OP_TYPE.DELETE: {
        const payload = this.payload as OpPayload[typeof OP_TYPE.DELETE];
        const { id, parentId } = payload;
        const delta = prev.get(id);
        if (!delta) return null;
        return new Op(OP_TYPE.INSERT, { delta, parentId });
      }
      case OP_TYPE.MOVE: {
        const payload = this.payload as OpPayload[typeof OP_TYPE.MOVE];
        const { x, y, ids } = payload;
        return new Op(OP_TYPE.MOVE, { ids, x: -x, y: -y });
      }
      case OP_TYPE.RESIZE: {
        const payload = this.payload as OpPayload[typeof OP_TYPE.RESIZE];
        const { id } = payload;
        const delta = prev.get(id);
        if (!delta) return null;
        const { x, y, width, height } = delta.getRect();
        return new Op(OP_TYPE.RESIZE, { id, x, y, width, height });
      }
      case OP_TYPE.REVISE: {
        const payload = this.payload as OpPayload[typeof OP_TYPE.REVISE];
        const { id, attrs } = payload;
        const delta = prev.get(id);
        if (!delta) return null;
        const prevAttrs: DeltaAttributes = {};
        for (const key of Object.keys(attrs)) {
          prevAttrs[key] = delta.getAttr(key);
        }
        return new Op(OP_TYPE.REVISE, { id, attrs: prevAttrs });
      }
      default:
        break;
    }
    return null;
  }
}

History

既然我们已经设计好了基于
Op
的原子化操作以及数据结构,那么紧接着我们就可以开始做
History
能力了,在这里首先需要注意我们先前对于
Invert
的思想是让其根据
DeltaSet
自动先生成
InvertOp
,在这里我们可以有两种方案来实现。

  1. 第一种方式是在应用
    Op
    之前我们先根据当前的
    DeltaSet
    自动生成一个
    InvertOp
    ,然后将这个
    Op
    交给
    History
    模块存储起来作为
    Undo
    的组操作即可。

  2. 第二种方式是我们在应用
    Op
    之前首先生成一遍新的
    Previous DeltaSet
    ,是一个
    immer
    的副本,然后将
    Prev DeltaSet
    以及
    Next DeltaSet
    一并作为
    OnChangeEvent
    交给
    History
    模块进行后续的操作。

最终我是选择了方案二作为整体实现,倒是没有什么具体依据,只是觉得这个
immer
的副本可能不仅会在这里使用,作为事件的一部分分发先前的数据值我认为是合理的,所以在应用
Op
的时候大致实现如下。

public apply(op: OpSetType, applyOptions?: ApplyOptions) {
    const options = applyOptions || { source: "user", undoable: true };
    const previous = new DeltaSet(this.editor.deltaSet.getDeltas());

    switch (op.type) {
      // 根据不同的`Op`执行不同的操作
    }

    this.editor.event.trigger(EDITOR_EVENT.CONTENT_CHANGE, {
      previous,
      current: this.editor.deltaSet,
      changes: op,
      options,
    });
}

其实我们也可以看到,整个编辑器内部的通信是依赖于
event
这个模块的,也就是说这个
apply
函数不会直接调用
History
的相关内容,我们的
History
模块是独立挂载
CONTENT_CHANGE
事件的。那么紧接着,我们需要设计
History
模块的数据存储,我们先来明确一下想要实现的内容,现在原子化的
Op
已经设计好了,所以在设计
History
模块时就不需要全量保存快照了,但是如果每个操作都需要并入
History Stack
的话可能并不是很好,通常都是有
N

Op
的一并
Undo/Redo
,所以这个模块应该有一个定时器与缓存数组还有最大时间,如果在
N
毫秒秒内没有新的
Op
加入的话就将
Op
并入
History Stack
,还有就是常规的
undo stack
以及
redo stack
,栈存储的内容也不应该很大,所以还需要设置最大存储量。

export class History {
  private readonly DELAY = 800;
  private readonly STACK_SIZE = 100;
  private temp: OpSetType[];
  private undoStack: OpSetType[][];
  private redoStack: OpSetType[][];
  private timer: ReturnType<typeof setTimeout> | null;
}

前边也提到过我们都是通过事件来进行通信的,所以这里需要先挂载事件,并且在这里将
Invert

Op
构建好,将其置入批量操作的缓存中。

  constructor(private editor: Editor) {
    this.editor.event.on(EDITOR_EVENT.CONTENT_CHANGE, this.onContentChange, 10);
  }

  destroy() {
    this.editor.event.off(EDITOR_EVENT.CONTENT_CHANGE, this.onContentChange);
  }
  
  private onContentChange = (e: ContentChangeEvent) => {
    if (!e.options.undoable) return void 0;
    this.redoStack = [];
    const { previous, changes } = e;
    const invert = changes.invert(previous);
    if (invert) {
      this.temp.push(invert);
      if(!this.timer) {
        this.timer = setTimeout(this.collectImmediately, this.DELAY);
      }
    }
  };

后来我在思考一个问题,如果这
N
毫秒内用户进行了
Undo
操作应该怎么办,后来想想实际上很简单,此时只需要清除定时器,将暂存的
Op[]
立即放置于
Redo Stack
即可。

  private collectImmediately = () => {
    if (!this.temp.length) return void 0;
    this.undoStack.push(this.temp);
    this.temp = [];
    this.redoStack = [];
    this.timer && clearTimeout(this.timer);
    this.timer = null;
    if (this.undoStack.length > this.STACK_SIZE) this.undoStack.shift();
  };

后边就是实际进行
redo

undo
的操作了,只不过在这里批量操作是使用循环每个
Op
都需要单独
Apply
的,这样感觉并不是很好,毕竟需要修改多次,虽然后边的渲染我只会进行一次批量渲染,但是这里事件触发的次数有点多,另外这里有个点还需要注意,我们在
History
模块里进行的操作,本身不应该再记入
History
中,所以这里还有一个
ApplyOptions
的设置需要注意。此外,在
undo
之后需要将这部分内容再次
invert
之后入
redo stack
,反过来也是一样的,此时我们直接取当前编辑器的
DeltaSet
即可。

  public undo() {
    this.collectImmediately();
    if (!this.undoStack.length) return void 0;
    const ops = this.undoStack.pop();
    if (!ops) return void 0;
    this.editor.canvas.mask.clearWithOp();
    this.redoStack.push(
      ops.map(op => op.invert(this.editor.deltaSet)).filter(Boolean) as OpSetType[]
    );
    this.editor.logger.debug("UNDO", ops);
    ops.forEach(op => this.editor.state.apply(op, { source: "undo", undoable: false }));
  }

  public redo() {
    if (!this.redoStack.length) return void 0;
    const ops = this.redoStack.pop();
    if (!ops) return void 0;
    this.editor.canvas.mask.clearWithOp();
    this.undoStack.push(
      ops.map(op => op.invert(this.editor.deltaSet)).filter(Boolean) as OpSetType[]
    );
    this.editor.logger.debug("REDO", ops);
    ops.forEach(op => this.editor.state.apply(op, { source: "redo", undoable: false }));
  }

最后

本文我们介绍总结了我们的图形编辑器中数据结构的设计以及
History
模块的实现,虽然暂时不涉及到
Canvas
本身,但是这都是作为编辑器本身的基础能力,也是通用的能力可以学习。后边我们可以介绍的能力还有很多,例如复制粘贴模块、画布分层、事件管理、无限画布、按需绘制、性能优化、焦点控制、参考线、富文本、快捷键、层级控制、渲染顺序、事件模拟、
PDF
排版等等,整体来说还是比较有意思的,欢迎关注我并留意后续的文章。

前记
最近给客户的几个产品出现了严重的质量问题。问题是产品在我们这边测试的好好的,到客户那边就出现问题了。后经历一起攻关分析,发现周围环境干扰会导致该设备出现异常。这中间,虽然说问题不全在我们这边。可本着客户就是上帝的面前,时刻检讨自身并作出一些修正。才能避免犯更大错误。  这让我想起来老东家,老大经常提的一个口号就是产品不是研发出来的,而是测试出来的。经过经过这几个跟头,笔者算是真正明白的产品充分测试的重要性和必要性。
跨边界意识
以前的产品相对简单一些。再加上合作的客户一般都有完备的测试流程。所以,大部分都我们这边把基本的功能测试可以,剩余的就交给客户来测试了。可最近几个产品,客户都是只懂销售和使用,对于技术的细节和需求的细节也是一窍不懂。这就对我们提出了更高的要求。这个前提下,团队还没有住那边思维方式。  团队在做产品研发的时候,思维还停留在以前的阶段,产品只要自己测试好了。就可以发给客户了。公司内部没有形成完整的测试流程。这种意识在大的项目面前,是非常危险的。这次经历,让我深刻的意识到,不要指望客户会对你的产品做充分的测试,不要指望单方面的一厢情愿认为产品就是这样的。要多的是要和客户的需求沟通。并做充分的实际使用场景验证。
充分测试
在做产品的方面。测试变得愈加重要。不能简单的测试ok了就交付给客户了。一定要针对产品的基本稳定性功能,整体性能,以及多场景使用的情况做多轮的测试。这些都好了再交付给客户。针对需求有分歧的地方,及时和客户做沟通交流和确认。这些都完备了。再把产品交付给客户。

前言

闲暇之余,简单写一个eventbus。

正文

什么是eventbus?

eventbus 是一个开源的发布订阅模式的框架,用于简化程序间不同组件的通信。
它允许不同组件间松耦合通信,组件之间不通过直接引用的方式,而是事件的方式进行消息传递。

下面进行代码演示:

首先是发布订阅,那么就应该有发布方法和订阅方法,因为是消息传递,那么就应该还有启动消费消息的方法。

public interface IEventBus : IDisposable
{
    Task Publish<T>(T @event) where T : IntegrationEvent;

    Task Subscribe<T>(IIntegrationEventHandler<T> handler)
        where T : IntegrationEvent;

    Task StartConsume();
}

大体我们要实现上面的功能。

然后我们可以定义事件的基础信息:

public class IntegrationEvent
{
    public Guid Id { get; set; }

    public DateTime OccurredOn { get; set; }

    public IntegrationEvent()
    {
        Id = Guid.NewGuid();
        OccurredOn = DateTime.Now;
    }
}

比如说要有唯一的id,同时要有事件发生的时间。

订阅的话,那么需要指定处理的对象。

public interface IIntegrationEventHandler
{
}

public interface IIntegrationEventHandler<in TIntegrationEvent> :
    IIntegrationEventHandler where TIntegrationEvent : IntegrationEvent
{
    Task Handler(TIntegrationEvent @event);
}

处理对象设计也很简单,就是需要创建一个有能够处理IntegrationEvent的对象即可。

这里很多人会疑惑,为什么很多框架的泛型接口类,往往会创建一个非泛型的接口。

这个其实是为了进一步抽象,方便做集合处理,下面将会介绍到。

然后就可以写一个内存型的eventbus。

public class InMemoryEventBus : IDisposable
{
    private Dictionary<string, List<IIntegrationEventHandler>>
        _dictionary = new Dictionary<string, List<IIntegrationEventHandler>>();

    public async Task Publish<T>(T @event) where T : IntegrationEvent
    {
        var fullName = @event.GetType().FullName;
        if (fullName == null)
        {
            return;
        }

        var handlers = _dictionary[fullName];

        foreach (var integrationEventHandler in handlers)
        {
            if (integrationEventHandler is IIntegrationEventHandler<T> handler)
            {
                await handler.Handler(@event);
            }
        }
    }

    public async Task Subscribe<T>(IIntegrationEventHandler<T> handler)
        where T : IntegrationEvent
    {
        var fullname = typeof(T).FullName;
        if (fullname == null)
        {
            return;
        }

        if (_dictionary.ContainsKey(fullname))
        {
            var handlers = _dictionary[fullname];
            handlers.Add(handler);
        }
        else
        {
            _dictionary.Add(fullname, new List<IIntegrationEventHandler>()
            {
                handler
            });
        }
    }

    public void Dispose()
    {
        // 移除相关连接等
    }
}

里面实现了eventbus的基本功能。可以看到上面的_dictionary,里面就是IIntegrationEventHandler,
所以泛型接口会继承一个非泛型的接口,是为了进一步抽象声明,对一些集合处理是很方便的。

然后这里为什么没有直接继承Ieventbus呢? 而是实现eventbus的功能。

因为Ieventbus 其实是面向用户的,继承ieventbus只是一个门面,相当于适配器。

而InMemoryEventBus 是为了实现功能。

可以理解为InMemoryEventBus 是我们电脑主板、cpu等,然后我们只需要一个实现其接口的组件,从而和外部连接。

而不是整个内核系统和外部直连,那么我们可以使用InMemoryEventBusClient 作为这个组件。

public class InMemoryEventBusClient : IEventBus
{
    private readonly InMemoryEventBus _eventBus;
    
    public InMemoryEventBusClient()
    {
        _eventBus = new InMemoryEventBus();
    }

    public void Dispose()
    {
        _eventBus.Dispose();
    }

    public async Task Publish<T>(T @event) where T : IntegrationEvent
    {
        await _eventBus.Publish(@event);
    }

    public async Task Subscribe<T>(IIntegrationEventHandler<T> handler) where T : IntegrationEvent
    {
        await _eventBus.Subscribe(@handler);
    }

    public Task StartConsume()
    {
        // 运行相关的消费
        return Task.CompletedTask;
    }
}

InMemoryEventBusClient 负责实现外部接口,InMemoryEventBus 负责实现功能。

从而达到解耦的目的。

同样的例子还有polly,这个框架应该很出名了,其中他里面就有很多衍生的组件,都是调用内核来适配其他框架定义的接口。

上面可以看到StartConsume什么都没有做,其功能被Publish给融合了。

只要publish就消费了。

如果我们扩展kafka的话,那么consume其实就是拉取数据然后消费,publish其实就是推向kafka,中间就是序列号和反序列话的过程。

eventbus 完善篇后续再补。

在C语言中,数据和数据的处理操作(函数)是分开声明的,在语言层面并没有支持数据和函数的内在关联性,我们称之为过程式编程范式或者程序性编程范式。C++兼容了C语言,当然也支持这种编程范式。但C++更主要的特点在支持基于对象(object-based, OB)和面向对象(object-oriented, OO),OB和OO的基础是对象封装,所谓封装就是将数据和数据的操作(函数)组织在一起,在语言层面保证了数据的访问和操作的一致性,这样从代码上更能表现出数据和函数的关系。在这里先不讨论在软件工程上这几种编程范式的优劣,我们先来分析对象加上封装后的内存布局,C++相对于C语言是否需要占用更多的内存空间,如果有,那么到底增加了多少内存成本?本文接下来将对各种情形进行分析。

空对象的内存布局

请看下面的代码,你觉得答案应该输出多少?

#include <iostream>
using namespace std;

class Object {
    // empty
};

int main() {
    Object object;
    cout << "The size of object is: " << sizeof(object) << endl;

    return 0;
}

答案是会输出:The size of object is: 1,是的,答案是1字节。在C++中,即使是空对象也会占用一定的空间,通常是1个字节。这个字节用来确保每个对象都有唯一的地址,以便在程序中进行操作。

含有数据成员的对象的内存布局

  • 非静态数据成员

现在再往这个类里面加入一些非静态的数据成员,来看看加入非静态的数据成员之后内存布局占用多少空间。

#include <iostream>
using namespace std;

class Object {
public:
    int a;
    int b;
};

int main() {
    Object object;
    cout << "The size of object is: " << sizeof(object) << endl;
    cout << "The address of object: " << &object << endl;
    cout << "The address of object.a: " << &object.a << endl;
    cout << "The address of object.b: " << &object.b << endl;

    return 0;
}

运行结果输出的是:

The size of object is: 8
The address of object: 0x16f07f464
The address of object.a: 0x16f07f464
The address of object.b: 0x16f07f468

现在object对象总共占用了8字节。int类型在我测试的机器上占用4字节的空间,这个跟测试的机器有关,有的机器有可能是8字节,在一些很老的机器上也有可能是2字节。

看后面三行的地址,可以看出,数据成员a的地址跟对象的地址是一样的,也就是说它是排列在对象的开始处,接下来是隔了4个字节后的地址,也就是数据成员b的地址,这说明数据成员a和b是顺序且紧密排列在一起的,并且是从对象的起始处开始的。结果表明,在这种情况下,C++的对象的内存布局跟C语言的结构的内存布局是一样的,并不会比C语言多占用一些内存空间。

  • 静态数据成员

C++的类也支持在类里面定义静态数据成员,那么定义了静态数据成员之后类对象的内存布局是怎么样的呢?在上面的类中加入一个静态数据成员,如以下代码:

class Object {
public:
    int a;
    int b;
    static int static_a;
};

运行结果输出:

The size of object is: 8
The address of object: 0x16b25f464
The address of object.a: 0x16b25f464
The address of object.b: 0x16b25f468
The address of object.static_a: 0x104ba8000

对象的大小结果还是8字节,说明静态成员变量并不会增加对象的内存占用空间。看下它们各个的地址,从结果可以看出,静态成员变量的地址跟非静态成员变量的地址相差很大,推断肯定不是和它们排列在一起的。在main函数中增加如下代码:

Object obj2;
cout << "The size of obj2 is: " << sizeof(obj2) << endl;
cout << "The address of obj2.static_a: " << &obj2.static_a << endl;

输出结果为:

The size of obj2 is: 8
The address of obj2.static_a: 0x104ba8000

定义了第2个对象,这个对象的大小也还是8字节,说明静态对象不是存储在每个对象中的,而是存在某个地方,由所有的同一个的类对象所共有的。从第2行输出的地址可以看出来,它的地址和第1个对象输出的地址是一样的,说明它们指向的是同一个变量。其实类中的静态数据成员是和全局变量一样存放在数据段中的,它的地址是在编译的时候就已经确定的了,每次运行都是一样的。它和全局变量一样,地址在编译时确定,所以访问它没有任何性能损失,和全局变量的区别是它的作用域不一样,类的静态数据成员的作用域只有在类中可见,访问权限受它在类中定义时的访问权限区段所控制。

含有成员函数的对象的内存布局

上面所讨论的都是类里面只有数据成员的情况,如果在类里再加上成员函数时,类对象的内存布局会有什么变化?在类中增加一个public的成员函数和一个静态成员函数,代码修改如下:

#include <iostream>
#include <cstdio>
using namespace std;

class Object {
public:
    void print() {
        cout << "The address of a: " << &a << endl;
        cout << "The address of b: " << &b << endl;
        cout << "The address of static_a: " << &static_a << endl;
    }

    static void static_func() {
        cout << "This is a static member function.\n";
    }

private:
    int a;
    int b;
    static int static_a;
};

int Object::static_a = 1;

int main() {
    Object object;
    cout << "The size of object is: " << sizeof(object) << endl;
    printf("The address of print: %p\n", &Object::print);
    printf("The address of static_func: %p\n", &Object::static_func);
    object.print();
    object.static_func();

    return 0;
}

运行输出结果如下:

The size of object is: 8
The address of print: 0x102d93120
The address of static_func: 0x102d931c4
The address of a: 0x16d06f464
The address of b: 0x16d06f468
The address of static_a: 0x102d98000
This is a static member function.

类对象的大小还是没变,还是8字节。说明增加成员函数并没有增加类对象的内存占用,无论是普通成员函数还是静态成员函数都一样。其实类中的成员函数并不存储在每个类对象中的,而是跟类的定义相关的,它是存放在可执行二进制文件中的代码段里的,由同一个类所产生出来的所有对象所共享。从上面输出结果中两个函数的地址来看,它们的地址很相近,说明普通成员函数和静态成员函数都是一样的,都存放在代码段中,地址在编译时就已确定。调用它们跟调用一个普通的函数没有什么区别,不会有性能上的损失。

含有虚函数的对象的内存布局

面向对象主要的特征之一就是多态,而多态的基础就是支持虚函数的机制。那么虚函数的支持对对象的内存布局会产生什么影响呢?这里先不分析虚函数的实现机制,我们先来分析内存布局的成本。在上面的例子中加入两个虚函数:一个普通的虚函数和虚析构函数,代码如下:

virtual ~Object() {
    cout << "Destructor...\n";
}

virtual void virtual_func() {
    cout << "Call virtual_func\n";
}

// 在main函数里增加两行打印
printf("The address of object: %p\n", &object);
printf("The address of virtual_func: %p\n", &Object::virtual_func);

编译运行,看看输出:

The size of object is: 16
The address of object: 0x16f97f458
The address of print: 0x100482f74
The address of static_func: 0x10048301c
The address of virtual_func: 0x10
The address of a: 0x16f97f460
The address of b: 0x16f97f464
The address of static_a: 0x100488000
Destructor...

在没有增加任何数据成员的情况下,对象的大小增加到了16字节,这说明虚函数的加入改变了对象的内存布局。那么增加的内容是什么呢?我们看到输出的打印中对象的首地址为0x16f97f458,而数据成员a的地址为0x16f97f460,这中间刚好差了8字节。而从上面的分析我们知道,原来a的地址是和对象的首地址是一样的,也就是说对象的内存布局是从a开始排列的,而现在在对象的起始地址和成员变量a之间空了8个字节,那么排在a之前的这8个字节的内容是什么呢?我们加点代码把它的内容输出出来,在main函数中加入以下代码:

long* p =  (long*)&object;
long* vptr = (long*)*p;
printf("vptr is %p\n", vptr);

输出结果:

The size of object is: 16
The address of object: 0x16b00f458
The address of print: 0x104df2f68
The address of static_func: 0x104df3010
The address of virtual_func: 0x10
The address of a: 0x16b00f460
The address of b: 0x16b00f464
The address of static_a: 0x104df8000
vptr is 0x104df4110
Destructor...

它的内容是0x104df4110,它其实是一个指针,在我的机器上占用8字节,在某些机器上可能是4字节。这个指针指向的其实是一个虚函数表,虚函数表是一个表格,表格里的每一项的内容存放的是每个虚函数的地址,这个地址指向虚函数真正的地址,在上面的打印中虚函数打印出来的地址是0x10,这个其实不是它的真正地址,是它在表格中的偏移地址。可以看到这个虚函数表地址和静态成员static_a的地址非常相近,其实虚函数表也是存放在数据段里面的,它在编译的时候由编译器确定好内容,并且编译器会自动扩充一些代码,在构造对象的时候把虚函数表的首地址插入到对象的起始位置。虚函数的详细分析在这里先不展开,后面再详细分析。从这里的分析可以看到,类里面增加虚函数,会在对象的起始位置上插入一个指针,对象的大小会增加一个指针的大小,为8字节或者4字节。如下面的示意图:
image

继承体系下的对象的内存布局

继承是C++中很重要的一个功能,按照不同的形式有单一继承、多重继承、虚继承,按照继承权限有public、protected、private。下面我们一一来分析,为简单起见,我们只分析public继承。

  • 单一继承
#include <iostream>
#include <cstdio>
using namespace std;

class point2d {
public:
    int x() { return x_; }
    int y() { return y_; }
protected:
    int x_;
    int y_;
};

class point3d: public point2d {
public:
    int z() { return z_; }

    void print() {
        printf("The address of x: %p\n", &x_);
        printf("The address of y: %p\n", &y_);
        printf("The address of z: %p\n", &z_);
    }
protected:
    int z_;
};

int main() {
    point2d p2d;
    point3d p3d;
    cout << "The size of p2d is: " << sizeof(p2d) << endl;
    cout << "The size of p3d is: " << sizeof(p3d) << endl;
    cout << "The address of p3d: " << &p3d << endl;
    p3d.print();

    return 0;
}

上面的代码编译运行输出:

The size of p2d is: 8
The size of p3d is: 12
The address of p3d: 0x16d2bb458
The address of x: 0x16d2bb458
The address of y: 0x16d2bb45c
The address of z: 0x16d2bb460

类point3d只有一个数据成员z_,但大小却有12字节,很明显它的大小是加上父类point2d的大小8字节的。从输出的地址看,p3d的地址是0x16d2bb458,从父类继承而来的x_的地址也是0x16d2bb458,这说明从父类继承而来的数据成员排列在前面,从对象的首地址开始,按照它们在类中的声明顺序依次排序,接着是子类自己的数据成员,从上面的结果看起来对象中的数据成员在内存中是按照顺序且紧凑的排列在一起的,如下图所示:
image
我们再来验证一下,把数据成员的声明类型改为char型,修改后输出结果:

The size of p2d is: 2
The size of p3d is: 3
The address of p3d: 0x16ba63467
The address of x: 0x16ba63467
The address of y: 0x16ba63468
The address of z: 0x16ba63469

看起来似乎我们的猜测是正确的,我们再继续修改,把x_改为int型,其它两个为char型,声明顺序还是跟之前一样,这次的输出结果:

The size of p2d is: 8
The size of p3d is: 12
The address of p3d: 0x16d033458
The address of x: 0x16d033458
The address of y: 0x16d03345c
The address of z: 0x16d033460

这次跟我们想要的结果不一样了,p2d的大小不是5字节而是8字节,p3d的大小不是6字节而是12字节,看起来编译器填充了内存空间使得他们的大小变大了。其实这时编译器为了访问效率选择了对齐,为了让变量的地址是4的倍数,它会填充中间的空挡,这些行为跟编译器有很大的关系,不同的编译器有不同的行为,类中数据成员的不同声明顺序和不同的数据类型可能就导致不同的结果。布局示意图如下:
image

  • 多重继承

接下来看看一个类继承了多个父类,它的内存布局是怎么样的。请看下面的代码:

#include <iostream>
#include <cstdio>
using namespace std;

class Base1 {
public:
    int b1;
};

class Base2 {
public:
    int b2;
};

class Derived: public Base1, public Base2 {
public:
    int d;
    void print() {
        printf("The address of b1: %p\n", &b1);
        printf("The address of b2: %p\n", &b2);
        printf("The address of d: %p\n", &d);
    }
};

int main() {
    Derived obj;
    printf("The size of obj is: %lu\n", sizeof(obj));
    printf("The address of obj: %p\n", &obj);
    obj.print();

    return 0;
}

输出结果:

The size of obj is: 12
The address of obj: 0x16f737460
The address of b1: 0x16f737460
The address of b2: 0x16f737464
The address of d: 0x16f737468

对象的总大小是12字节,它是子类自身拥有的一个数据成员4字节加上分别从两个父类继承而来的两个数据成员共8字节的总和。从输出的地址可以看出来,从父类Base1继承来的成员b1和对象的首地址相同,接着是从父类Base2继承而来b2,最后是子类自己的成员d,说明对象的布局是从b1开始,然后是b2,最后是d,这个跟继承的顺序有关,第一继承而来的数据成员排在最前面,按照在类中声明的顺序依次排列,其次是第二继承而来的数据成员,以此类推,最后是子类自己的数据成员。布局示意图如下:
image

  • 父类带虚函数的继承

如果父类中带有虚函数,那么对子类的内存布局有何影响?在上面的代码中的两个父类各加上一个虚函数,而子类暂时先不加虚函数,如下代码:

// 在class Base1中加入以下代码
virtual void virtual_func1() {
    printf("This is virtual_func1\n");
}

// 在class Base2中加入以下代码
virtual void virtual_func2() {
    printf("This is virtual_func2\n");
}

编译运行,输出结果:

The size of obj is: 32
The address of obj: 0x16b807448
The address of b1: 0x16b807450
The address of b2: 0x16b807460
The address of d: 0x16b807464

这次对象的大小竟然是32字节,比上面的例子增加了20字节,这里并没有增加任何数据成员,只是仅仅在父类增加了虚函数,根据上面的分析,增加虚函数会引入虚函数表指针,指针占8字节的大小,那为什么会增加这么多呢?我们可以借助工具来分析一下,编译器一般会提供一些辅助分析工具供开发人员使用,其中有一个功能是把每个类的布局给打印出来,gcc、clang、vs都有类似的命令,clang可以使用下面的命令来查看:

clang -Xclang -fdump-record-layouts -stdlib=libc++ -std=c++11 -c filename.cpp

输出的结果很多,我截取关键的一部分:
image
上图中,左边的数字就是对象的成员相对于对象的起始地址的偏移量。从上图我们可以得出以下的结论:

1.父类中各有一个虚函数表以及一个指向它的虚函数表指针,子类分别从父类中继承下来,父类有多少个虚函数表,子类就有多少个虚函数表。这里额外插一句,子类虽然继承了父类的虚函数表,但子类的虚函数表不会和父类的虚函数表是同一个,就算子类没有覆盖父类的任何虚函数,编译器也会复制多一份虚函数表出来,尽管它们的虚函数表的内容是一模一样的,但是一般情况下子类都会覆盖父类的虚函数,不然也没有必要用虚函数了,虚函数具体的分析以后再讲。

2.编译器为了访问效率选择了8字节的对齐,也就是说成员变量b1占了8字节,数据本身占了4字节,为了对齐填充了4字节,使得下一个虚函数表指针可以对齐访问。

所以,分析的结论就是子类对象的内存布局是这样的,首先是从Base1父类继承来的虚函数表指针,占用8字节,接着是继承来的b1成员变量,加上填充的4字节共占用了8字节,再接着是从父类Base2继承来的虚函数表指针,占用8字节,之后是继承的b2成员变量,占用4字节,子类自己的成员变量d紧跟着排列在后面,总共32字节。布局示意图如下:
image

虚继承的对象的内存布局

虚继承是为了解决棱形继承情形下重复继承的问题提出来的解决办法,如下面的代码:

#include <iostream>
#include <cstdio>
using namespace std;

class Grand {
    int a;
};

class Base1: public Grand {
};

class Base2: public Grand {
};

class Derived: public Base1, public Base2 {
};

int main() {
    Grand g;
    Base1 b1;
    Base2 b2;
    Derived obj;
    //obj.a = 1;	// 这行编译不过。
    printf("The size of g is: %lu\n", sizeof(g));
    printf("The size of b1 is: %lu\n", sizeof(b1));
    printf("The size of b2 is: %lu\n", sizeof(b2));
    printf("The size of obj is: %lu\n", sizeof(obj));
    return 0;
}

上面的代码中如果不把第23行代码屏蔽掉是编译不过的,因为Base1和Base2都继承了Grand,Derived又继承了Base1和Base2,Grand中的成员a将会被重复继承两次,这时在子类Derived中就存在了两个成员a,这时从Derived访问a就会出现错误,因为编译器不知道你要访问的是哪一个a,出现了名字冲突的问题。屏蔽掉第23行后编译运行,看下输出结果:

The size of g is: 4
The size of b1 is: 4
The size of b2 is: 4
The size of obj is: 8

从结果中也可以验证,子类Derived占了两倍的大小。为了解决像这种重复继承了两次的问题,办法是引入虚继承,我们修改下代码继续分析:

#include <iostream>
#include <cstdio>
using namespace std;

class Grand {
public:
    int a;
};

class Base1: virtual public Grand {
public:
    int b;
};

class Base2: virtual public Grand {
public:
    int c;
};

class Derived: public Base1, public Base2 {
public:
    int d;
};

int main() {
    Grand g;
    Base1 b1;
    Base2 b2;
    Derived obj;
    obj.a = 1;
    printf("The size of g is: %lu\n", sizeof(g));
    printf("The size of b1 is: %lu\n", sizeof(b1));
    printf("The size of b2 is: %lu\n", sizeof(b2));
    printf("The size of obj is: %lu\n", sizeof(obj));
    printf("The address of obj: %p\n", &obj);
    printf("The address of obj.a: %p\n", &obj.a);
    printf("The address of obj.b: %p\n", &obj.b);
    printf("The address of obj.c: %p\n", &obj.c);
    printf("The address of obj.d: %p\n", &obj.d);
    
    return 0;
}

这时访问Derived类的对象中的成员变量a就没有冲突了,如上面代码的第30行,上面代码的输出结果:

The size of g is: 4
The size of b1 is: 16
The size of b2 is: 16
The size of obj is: 40
The address of obj: 0x16d70b420
The address of obj.a: 0x16d70b440
The address of obj.b: 0x16d70b428
The address of obj.c: 0x16d70b438
The address of obj.d: 0x16d70b43c

改为虚继承后,obj.a = 1;这行代码能编译通过了,不会出现名字冲突了。我们来看看孙子类Derived的对象的大小,竟然是40字节,增大了这么多,还是使用上面的命令来dump出对象的内存布局,结果如下图,截取部分:
image
这里先补充一点,虚继承是借助于虚基类表来实现,被虚继承的父类的成员变量会放在虚基类表中,通过在对象中插入的虚基类表指针来访问虚基类表,有点类似于虚函数表,实现方式不同的编译器采用不一样的方式,gcc和clang是虚函数表和虚基类表共用一个表,称为虚表,所以只需要一个指针指向它,叫做虚表指针,而Windows平台的Visual Studio是采用两个表,所以Windows下对象里会有两个指针,一个虚函数表指针和一个虚基类表指针,虚基类的实现细节后面再详细分析。

从上图可以看到,孙子类Derived的对象的内存里拥有两个虚表指针,因为父类Base1和Base2分别虚继承了爷爷类Grand,每一个虚继承将会产生一个虚表指针,按照继承的顺序依次排列,首先是Base1子对象的内容,包含了一个虚表指针和成员变量b,b之后会填充4字节到8字节对齐,然后是Base2子对象的内容,同样也包含了一个虚表指针和成员变量c,再之后是孙子类Derived自己的成员变量d,它是紧凑的排列在c之后的,最后是爷爷类Grand中的成员变量a,可以看到虚继承下来的成员变量被安排到最后的位置了,从打印的地址也可以看出来。布局示意图如下:
image

此篇文章同步发布于我的微信公众号:
C++对象封装后的内存布局

如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享或者微信号iTechShare并关注,以便在内容更新时直接向您推送。