【数据析要】CASIA-HWDB 中文手写数据集一站式处理指南

一、CASIA 数据集介绍

CASIA-OLHWDB(在线)与 CASIA-HWDB(离线)中文手写数据库由中国科学院自动化研究所模式识别国家实验室(NLPR, CASIA)构建。
1020 名书写者使用 Anoto 数码笔在专用纸张上书写,因而同时获得了在线笔迹和离线图像。样本涵盖孤立字符与连续文本两种形式。数据采集于 2007–2010 年,2010 年完成切分与标注。

整套数据库共 12 个子集:

  • 在线、离线各 6 个;
  • 其中 3 个为孤立字符(DB1.0–1.2),3 个为连续文本(DB2.0–2.2)。

规模一览(在线或离线均同)

  • 孤立字符:约 390 万个样本、7 356 类(7 185 个汉字+171 个符号)。
  • 连续文本:约 5 090 页图像、135 万个字符级样本。

所有数据均已完成字符级切分与标注,并提供了标准的训练集/测试集划分。

二、CASIA 数据集下载

下载地址:http://www.nlpr.ia.ac.cn/databases/handwriting/Download.html

本文仅处理离线部分数据集 HWDB1.x 和 HWDB2.x。

  • HWDB1.x
    在这里插入图片描述

  • HWDB2.x
    在这里插入图片描述

三、CASIA-HWDB1.x 数据集处理

3.1 HWDB1.x 基本信息

离线手写汉字数据库HWDB1.x中包含三个孤立字符数据集,其统计信息如下表所示:

该数据集共包含 1,020 个文件,每个文件(*.gnt)存储了一位书写者所写字符的灰度图像,图像按顺序串联存储。

离线孤立字符数据集统计信息:

数据集书写者数量样本总数符号数汉字数/类别数
HWDB1.04201,680,25871,1221,609,136 / 3,866
HWDB1.13001,172,90751,1581,121,749 / 3,755
HWDB1.23001,041,97050,981990,989 / 3,319
总计1,0203,895,135173,2613,721,874 / 7,185

字符集说明:

  • HWDB1.0:包含 3,866 个汉字和 171 个字母数字及符号。其中 3,740 个汉字属于 GB2312-80 一级字集(共 3,755 字)。
  • HWDB1.1:包含 3,755 个 GB2312-80 一级汉字和 171 个字母数字及符号。
  • HWDB1.2:包含 3,319 个汉字和 171 个字母数字及符号。其汉字集合与 HWDB1.0 完全不重叠。
  • HWDB1.0 + HWDB1.2 共包含 7,185 个汉字,覆盖了 GB2312 标准中全部 6,763 个汉字。

3.2 HWDB1.x 处理代码

3.2.0 *.gnt文件格式说明与处理思路

*.gnt 文件格式说明:

字段类型长度示例说明
本样本总字节数unsigned int4 B0x00000462从本字段开始到下一个样本开始之前的字节数(含自身 4 B)
标签/内码char2 B0xb0a1(对应汉字“啊”)GB 编码,低字节在前,高字节在后
宽度 Widthunsigned short2 B0x0040 = 64图像一行所占像素数
高度 Heightunsigned short2 B0x0040 = 64图像总行数
位图 BitmapcharWidth×Height B灰度值按行主序(row-major)连续存储,0=黑,255=白

读取思路:

  1. 先读 4 B 拿到“样本总长度”,就能准确定位下一个样本的起始偏移。
  2. 接着读 2 B 的 GB 编码,注意 Little-Endian(低位在前)。
  3. 再读 2 B 宽、2 B 高,就能算出后续位图大小。
  4. 最后一次性读取 Width×Height 个字节,reshape 成 (H, W) 的 NumPy 数组即可直接可视化。

3.2.1 统计字符总数,生成字典

import struct
import os
import numpy as np
from tqdm import tqdm
import json

ROOT_PATH = "CASIA_Handwriting/HWDB1_x"
DATA_PATH = os.path.join(ROOT_PATH, 'data')

def read_from_gnt_dir(gnt_dir=DATA_PATH):
    def one_file(f):
        header_size = 10
        while True:
            header = np.fromfile(f, dtype='uint8', count=header_size)
            if not header.size: 
                break
            sample_size = header[0] + (header[1]<<8) + (header[2]<<16) + (header[3]<<24)
            tagcode = header[5] + (header[4]<<8)
            width = header[6] + (header[7]<<8)
            height = header[8] + (header[9]<<8)
            assert header_size + width*height == sample_size, 'Header + data size not equal sample_size!'
            image = np.fromfile(f, dtype='uint8', count=width*height).reshape((height, width))
            yield image, tagcode

    for file_name in os.listdir(gnt_dir):
        if file_name.endswith('.gnt'):
            file_path = os.path.join(gnt_dir, file_name)
            with open(file_path, 'rb') as f:
                for image, tagcode in one_file(f):
                    yield image, tagcode

# 生成字符字典
char_set = set()
for _, tagcode in tqdm(read_from_gnt_dir(), total=4000000):
    tagcode_unicode = struct.pack('>H', tagcode).decode('gbk')
    char_set.add(tagcode_unicode)

char_list = list(char_set)
char_dict = dict(zip(sorted(char_list), range(len(char_list))))

# 存储成json
json_str = json.dumps(char_dict, ensure_ascii=False)
with open(f'{ROOT_PATH}/char_dict.json', 'w') as json_file:
    json_file.write(json_str)

3.2.2 获取单字符图像并拼接

控制每张图片上的字符数量为80(8行*10列)

import struct
import os
import numpy as np
from tqdm import tqdm
import json
import cv2
import random

ROOT_PATH = "CASIA_Handwriting/HWDB1_x"
DATA_PATH = os.path.join(ROOT_PATH, 'data')
LABEL_DIR  = os.path.join(ROOT_PATH, 'labels')
IMAGE_DIR  = os.path.join(ROOT_PATH, 'images')

os.makedirs(IMAGE_DIR, exist_ok=True)
os.makedirs(LABEL_DIR, exist_ok=True)

# load char_dict
with open(f'{ROOT_PATH}/char_dict.json', 'r') as f:
    char_dict = json.load(f)

def read_from_gnt(gnt_path):
    image_infos = []
    with open(gnt_path, 'rb') as f:
        header_size = 10
        while True:
            header = np.fromfile(f, dtype='uint8', count=header_size)
            if not header.size: 
                break
            sample_size = header[0] + (header[1]<<8) + (header[2]<<16) + (header[3]<<24)
            tagcode = header[5] + (header[4]<<8)
            width = header[6] + (header[7]<<8)
            height = header[8] + (header[9]<<8)
            tagcode_unicode = struct.pack('>H', tagcode).decode('gbk')
            assert header_size + width*height == sample_size, 'Header + data size not equal sample_size!'
            image = np.fromfile(f, dtype='uint8', count=width*height).reshape((height, width))
            # 过滤掉非中文字符
            if char_dict[tagcode_unicode] >= 169 and char_dict[tagcode_unicode] <= 7353:
                image_infos.append([image, tagcode_unicode, width, height])
    return image_infos

def concat_images(image_infos, base_name):
    random.shuffle(image_infos)
    total_num = len(image_infos) // 80
    # print("total_num=", total_num)
    ratio = 0.8
    padding = 20    # 间隙距离
    for i in range(total_num):
        infos = image_infos[i*80:(i+1)*80]
        images = [info[0] for info in infos]
        labels = [info[1] for info in infos]
        widths = [info[2] for info in infos]
        heights = [info[3] for info in infos]
        # 计算拼接后图像的宽和高
        width = int((np.array(widths).max() - np.array(widths).min()) * ratio + np.array(widths).min())
        height = int((np.array(heights).max() - np.array(heights).min()) * ratio + np.array(heights).min())
        # 拼接图像
        final_image = np.zeros((height*8 + 9*padding, width*10 + 11*padding), dtype=np.uint8)
        # print(final_image.shape)
        final_image[:, :] = 255
        for j in range(80):
            row = j // 10
            col = j % 10
            # 对image[j]的大小做改变
            images[j] = cv2.resize(images[j], (width, height))
            final_image[padding*(row+1)+height*row:padding*(row+1)+height*(row+1), padding*(col+1)+width*col:padding*(col+1)+width*(col+1)] = images[j]
        # 拼接label
        all_label = []
        for j in range(8):
            all_label.append(' '.join(labels[j*10:(j+1)*10]))

        # 保存图像
        img_path   = os.path.join(IMAGE_DIR, f'{base_name}_{i:03d}.jpg')
        label_path = os.path.join(LABEL_DIR, f'{base_name}_{i:03d}.txt')
        cv2.imwrite(img_path, final_image)
        with open(label_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(all_label))
    return total_num

files = os.listdir(DATA_PATH)
file_paths = [os.path.join(DATA_PATH, file) for file in files]
print(f"一共找到{len(file_paths)}个文件")
total_count = 0
for file_path in tqdm(file_paths, total=len(file_paths)):
    try:
        image_infos = read_from_gnt(file_path)
        total_num = concat_images(image_infos, os.path.basename(file_path).split('.')[0])
        total_count += total_num
    except Exception as e:
        print(f"❌ 处理文件失败: {file_path}, 错误: {e}")
        continue
print(f"一共生成{total_count}个样本")

3.3 拼接结果示例

在这里插入图片描述

四、CASIA-HWDB2.x 数据集处理

4.1 HWDB2.x 基本信息

离线文本数据集由“孤立字符数据集”同一批书写者完成。 每人抄写 5 页给定文本;由于数据丢失,缺 1 名书写者(编号 371)及 4 页图像,共少 5 页。 每页保存为一个 *.dgrl 文件,文件名格式为“书写者索引_页码.dgrl”。
除灰度图像外,文件内还自带文本行切分 ground-truth 和每个字符的类别标签(GB 码)

离线手写文本数据集统计信息:

数据集书写者数页数文本行数字符样本数 / 类别数集外样本数
HWDB2.04192,09220,495538,868 / 1,2221,106
HWDB2.13001,50017,292429,553 / 2,310172
HWDB2.23001,49914,443380,993 / 1,331581
合计1,0195,09152,2301,349,414 / 2,7031,859

“集外样本”指不属于 HWDB1.0-1.2 共 7,356 类的字符。

4.2 HWDB2.x 处理代码

4.2.0 *.dgrl文件格式说明

字段类型长度说明 / 示例
文件头(File Header)
Header 总字节数int4 B36 + strlen(illustration)
格式标识char[8]8 B固定字符串 “DGRL”
说明文本char*任意以 ‘\0’ 结尾的 ASCII 描述
编码类型char[20]20 B“ASCII”、“GB” 等
编码字节数short2 B通常 1(GB1)、2(GBK)
每像素位数short2 B1=二值,8=灰度
图像记录(Image Records)
文档高int4 B整页图像像素行数
文档宽int4 B整页图像像素列数
行数int4 B本页文本行总量
行记录(Line Records,循环重复)
本行字符数int4 B后续码序列长度
字符编码char[]CodeLen×CharNum每字符 CodeLen 字节;无效字符填 0xFF
左上角坐标int×24 B+4 Btop, left(相对于整页)
行高int4 B本行 bitmap 高度 H
行宽int4 B本行 bitmap 宽度 W
位图数据BYTE[]H × ((W+7)/8) 或 H×W二值按字节位打包;灰度按像素顺序存储

注意点:

  • 背景像素值固定为 255,前景笔画灰度 0-254。
  • 每页按“行”顺序存储;行与行之间在垂直方向可能出现笔画重叠,因此还原整页图像时,需把多行前景像素“按位或”方式合并,而非简单拼贴。

4.2.2 获取单行图像并拼接

import struct
import os
import cv2
import numpy as np
from tqdm import tqdm

ROOT_PATH = 'CASIA_Handwriting/HWDB2_x'
DATA_PATH = os.path.join(ROOT_PATH, 'data')
LABEL_DIR  = os.path.join(ROOT_PATH, 'labels')
IMAGE_DIR  = os.path.join(ROOT_PATH, 'images')

os.makedirs(IMAGE_DIR, exist_ok=True)
os.makedirs(LABEL_DIR, exist_ok=True)

def read_from_dgrl(dgrl):
    if not os.path.exists(dgrl):
        print('DGRL not exist!')
        return
    
    base_name = os.path.basename(dgrl).replace('.dgrl', '')
    with open(dgrl, 'rb') as f:
        # 1. header size
        header_size = int(np.fromfile(f, dtype='<u4', count=1))   # 小端 uint32
        # 2. skip 剩余 header,并取 code_length
        header = np.fromfile(f, dtype='uint8', count=header_size - 4)
        code_length = int(header[-4]) + (int(header[-3]) << 8)   # 2 字节小端
        # 3. 图像基本信息
        height, width, line_num = np.fromfile(f, dtype='<u4', count=3)
        # print(f'[INFO] {base_name}  高={height} 宽={width} 行数={line_num}')

        # 4. 依次读取每行:label / 位置 / 像素
        full_img  = np.zeros((height, width), dtype=np.uint8)
        full_img[:, :] = 255
        all_label = []

        last_y = 0
        first_y = 0
        for i in range(line_num):
            # 4.1 字符数
            char_num = int(np.fromfile(f, dtype='<u4', count=1))
            # 4.2 读取该行的标注信息
            label = np.fromfile(f, dtype='uint8', count=code_length*char_num)
            label = [label[i]<<(8*(i%code_length)) for i in range(code_length*char_num)]
            label = [sum(label[i*code_length:(i+1)*code_length]) for i in range(char_num)]
            label = [struct.pack('I', i).decode('gbk', 'ignore')[0] for i in label]
            label = ''.join(label)
            label = ''.join(label.split(b'\x00'.decode()))	# 去掉不可见字符 \x00,这一步不加的话后面保存的内容会出现看不见的问题
            all_label.append(label)
            # 4.3 位置
            y, x, h, w = np.fromfile(f, dtype='<u4', count=4)
            if i == 0:
                first_y = y
            # print(y, x, h, w)
            # 4.4 位图
            bitmap = np.fromfile(f, dtype='uint8', count=h*w).reshape(h, w)
            # 处理y重叠现象
            if y < last_y:
                y = last_y
            last_y = y + h
            # print("last_y: ", last_y)
            if last_y > height:
                # 扩充图像
                full_img = np.pad(full_img, ((0, last_y + 300 - height), (0, 0)), 'constant', constant_values=255)
                # print(f"填充后高度:{height} -> {last_y + 300}")
                height = last_y + 300
            full_img[y:y + h, x:x + w] = bitmap

    # 裁剪图像, 使得上下边界合理
    full_img = full_img[max(first_y - 300, 0): min(last_y + 300, height), :]

    # 5. 保存
    img_path   = os.path.join(IMAGE_DIR, f'{base_name}.jpg')
    label_path = os.path.join(LABEL_DIR, f'{base_name}.txt')
    cv2.imwrite(img_path, full_img)
    with open(label_path, 'w', encoding='utf-8') as f:
        f.write('\n'.join(all_label))
    # print(f'[SAVE] 图像 -> {img_path}  标签 -> {label_path}')

files = os.listdir(DATA_PATH)
file_paths = [os.path.join(DATA_PATH, file) for file in files]
for file_path in tqdm(file_paths, total=len(file_paths)):
    try:
        read_from_dgrl(file_path)
    except Exception as e:
        print(f"❌ 处理文件失败: {file_path}, 错误: {e}")
        continue

4.3 拼接结果示例

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值