简介:一份汇集全国众多高校标志图与校徽的doc文档,面向高校师生、校史研究者、设计从业者及需要快速辨识院校标识的读者。它解决校徽资料分散、难以集中查阅的问题,可按校名快速定位北京大学、清华大学、复旦大学、浙江大学等院校的徽标样式。文档共1个doc文件,压缩包约21.96MB,虽为单一文档,但以图为主、体量可观,便于离线浏览与检索。已有1517人学习下载,说明其在院校标识整理方面具备一定参考热度。内容覆盖综合性大学、理工、师范、农林、医药、财经、政法、艺术、体育及民族院校,既能观察校徽中的红楼、飞机、通信、建筑、树木、蛇杖、草药、舞蹈、运动等视觉元素,也能借此理解各校历史沿革、校训精神、地域特色与学科优势。对做校史梳理、海报展板、社团招新、课件配图或设计调研的人而言,可作为一份较全的校徽图像索引与灵感素材库。
1. 一份「校徽大全.doc」为什么撑不起真正的图片资产库
设计同事要一份高校校徽做展板,行政要几十所高校标志做汇报底图,你翻出《中国大学最全最完整的标志图+校徽(最全).doc》,打开是几百页图片,另存出来一堆 image1.png、image2.png。反直觉的地方在于:这份文档的「全」是给人眼看的全,对程序来说它是一份没有主键、没有索引、没有元数据的二进制图片堆——不能按学校名查,不能拿一张图反查是哪所学校,也没法把明年新增的院校平滑并进来。要把它变成能用的资产,得走三步:先把图从文档里干净地抠出来,再做规范化和去重,最后建一层按校名、按图片双通道检索的索引。做设计资源站、内容平台、校内信息化的人需要这套流程,第一次做图片数据集清洗的同学也可以照着复现,每一段都有可以直接抄的命令和参数。
2. 从 .doc 与 .docx 里批量抠出校徽原图
2.1 先确认拿到的是 .doc 还是 .docx
扩展名经常骗人:有人把 .docx 改名成 .doc,也有人保存成 .doc 但实际是 OOXML。处理路线完全不同,先看魔数。
file -b --mime-type "中国大学最全最完整的标志图+校徽(最全).doc" # application/msword -> 旧二进制 .doc # application/vnd.openxmlformats-officedocument.wordprocessingml.document -> .docx xxd -l 8 "中国大学最全最完整的标志图+校徽(最全).doc" # d0cf 11e0 a1b1 1e00 -> OLE2 复合文档,老 .doc # 504b 0304 1400 0600 -> ZIP 包,OOXML(.docx/.pptx/.xlsx 同源)判断依据就两条:D0CF11E0是 OLE 复合文档头,图片被塞在 WordDocument 流的二进制结构里,没有公开稳定的解析库能直接把图按顺序取出来;504B0304是 ZIP 本地文件头,图片就是包里的普通文件,直接读。
| 真实格式 | 图片存放位置 | 推荐提取方式 |
|---|---|---|
| OLE2(.doc) | WordDocument 流内嵌 | LibreOffice 转 docx 后再拆包 |
| OOXML(.docx) | word/media/ | zipfile直接读 |
| RTF / MHT | 内联十六进制或 base64 | 正则 + 解码,少见但存在 |
2.2 .doc 路线:headless 转换后再拆包
老 .doc 不要硬啃二进制,转一道成本最低。
# 单文件:转成同名 docx,输出到 out/ soffice --headless --convert-to docx --outdir ./out "input.doc" # 批量:容器里并发跑一定要指定独立 profile,否则会互相抢锁直接失败 for f in ./raw/*.doc; do soffice --headless \ -env:UserInstallation=file:///tmp/lo_profile_$$ \ --convert-to docx --outdir ./out "$f" done--headless表示不弹 GUI;--convert-to docx指定目标格式,也可以换成docx:"MS Word 2007 XML"显式指定过滤器;-env:UserInstallation是关键参数,默认 profile 是全局的,两个进程同时启动会有一个静默退出,日志里只留一行 lock 相关提示,很容易误判成「文件损坏」。转换后再走 2.3 的 OOXML 流程即可,图片二进制本身不会被重编码,原图质量保留。
2.3 .docx 路线:zipfile 直读 media 和关系表
OOXML 里图片文件名是image1.png这种流水号,和学校名没有任何关系。顺序信息藏在word/document.xml的段落流里,图片引用节点a:blip的r:embed属性指向word/_rels/document.xml.rels里的关系 id。按段落顺序遍历,就能把「图片」和「它附近的文字」绑在一起。
import zipfile, csv, pathlib from lxml import etree NS = { 'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main', 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main', 'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', 'rel': 'http://schemas.openxmlformats.org/package/2006/relationships', } def extract(docx_path, out_dir): out_dir = pathlib.Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(docx_path) as z: # 1) 关系表:rId -> media/imageN.png rels = etree.fromstring(z.read('word/_rels/document.xml.rels')) rid2media = { r.get('Id'): r.get('Target') for r in rels.findall('rel:Relationship', NS) if 'media/' in (r.get('Target') or '') } # 2) 正文按段落顺序遍历,记录当前上下文文本 doc = etree.fromstring(z.read('word/document.xml')) rows, ctx = [], '' for p in doc.iter('{%s}p' % NS['w']): text = ''.join(t.text or '' for t in p.iter('{%s}t' % NS['w'])).strip() if text and len(text) <= 40: # 短段落多半是学校名或小标题 ctx = text for blip in p.iter('{%s}blip' % NS['a']): target = rid2media.get(blip.get('{%s}embed' % NS['r'])) if not target: continue rows.append((ctx, target, z.read('word/' + target.lstrip('/')))) # 3) 用序号命名落盘,真实校名后面再对齐,先保证不漏图 with open(out_dir / 'manifest_raw.csv', 'w', newline='', encoding='utf-8') as f: w = csv.writer(f) w.writerow(['ctx', 'target', 'nbytes']) for i, (ctx, target, data) in enumerate(rows): ext = pathlib.Path(target).suffix.lower() or '.png' (out_dir / f'{i:05d}{ext}').write_bytes(data) w.writerow([ctx, target, len(data)]) return len(rows)关键点是a:blip的r:embed,它只存关系 id 不存路径,必须借助 rels 表翻译;ctx用「最近的短段落文本」做启发式,中文学校名一般不超过 12 个字,40 字是留了余量。别把ctx直接当主键,文档里经常出现「985 高校」「华东地区」这类分组标题,也会被当成上下文抓进来。
2.4 学校名对齐:四种来源,可靠度不一样
| 对齐来源 | 可靠度 | 说明 |
|---|---|---|
图片的docPr name/descr属性 | 高 | 作者规范时会是学校名,解析wp:docPr即可 |
| 图片紧邻的段落文本 | 中 | 最常见来源,需过滤分组标题 |
| 文档目录页/清单页文本 | 中 | 顺序未必与正文一致,要按序号对齐 |
| 人工核对 | 高 | 300~3000 条量级完全可接受 |
实践中我一般先把前两种跑出来,生成一份待确认.csv,只人工过一遍冲突和空值的行,通常能收敛到 95% 以上。剩下的交给第 4 章的按图检索去反查,会比人眼翻文档快得多。
3. 校徽图清洗、规范化与感知哈希去重
3.1 三步规范化:去白边、补方、定尺寸
抠出来的原图大小不一,有的是 2000×2000 带大片留白,有的是 180×180 贴边。不统一尺寸,展示时会出现「有的校徽大有的小」的观感问题。
from PIL import Image, ImageChops def normalize(src, dst, size=512, bg=(255, 255, 255, 0), tol=12): im = Image.open(src).convert('RGBA') # 1) 找内容边界:有透明通道用 alpha,否则和左上角像素做差 if im.getchannel('A').getextrema()[0] < 255: bbox = im.getchannel('A').getbbox() else: ref = Image.new('RGBA', im.size, im.getpixel((0, 0))) diff = ImageChops.difference(im, ref).convert('L') bbox = diff.point(lambda v: 255 if v > tol else 0).getbbox() if bbox: im = im.crop(bbox) # 2) 等比缩到 94% 内接,再居中补成正方形,视觉大小才一致 inner = int(size * 0.94) im.thumbnail((inner, inner), Image.LANCZOS) canvas = Image.new('RGBA', (size, size), bg) canvas.paste(im, ((size - im.width) // 2, (size - im.height) // 2), im) # 3) 统一输出 PNG,保留透明通道;要 JPG 就先铺白底再转 canvas.save(dst, 'PNG', optimize=True)参数含义:size=512是展示与体积的折中,做印刷素材另存一份 2048 的原图;tol=12是 RGB 差值阈值,扫描件或带纹底的图噪点多,调到 24~32 更稳;0.94留安全边,避免圆形校徽顶到画布边缘被裁。透明底和白色底建议各出一份,PPT 场景白底更好用,深色背景海报用透明底。
3.2 命名规范与别名表
统一命名是后面所有查询的基础。主键用中文全称,文件名用 slug,两个都不能省。
| 字段 | 示例 | 说明 |
|---|---|---|
name_zh | 北京大学 | 主键语义,唯一 |
slug | beijingdaxue | 拼音全拼,做文件名与 URL |
aliases | 北大|PKU|Peking University | 竖线分隔,覆盖简称与英文名 |
emblem_path | emblem/beijingdaxue.png | 纯图形徽标 |
logo_path | logo/beijingdaxue.png | 带校名的组合标志 |
phash | 64 bit 整数 | 按图检索用 |
sha256 | 十六进制串 | 增量更新用 |
slug 用全拼而不是缩写,是为了避免PKU、THU这类缩写冲突和大小写陷阱;缩写放 aliases 里做召回就够了。
3.3 pHash 去重:阈值怎么定
同一所学校往往出现三四张图:彩色版、单色描边版、带校名的横版、老版校徽。先算 64 位感知哈希,再比汉明距离。
import imagehash, itertools from PIL import Image def hashes(path): g = Image.open(path).convert('L') return imagehash.phash(g, hash_size=8), imagehash.dhash(g, hash_size=8) def dedupe(paths, thresh=6): h = {p: hashes(p) for p in paths} out, used = [], set() for a, b in itertools.combinations(paths, 2): if a in used and b in used: continue dp = h[a][0] - h[b][0] # pHash 汉明距离 dd = h[a][1] - h[b][1] # dHash 汉明距离 if dp <= thresh and dd <= thresh: # 双票通过才判重 out.append((a, b, dp, dd)) used.update([a, b]) return out| 汉明距离 | 判断 | 处理动作 |
|---|---|---|
| 0–2 | 基本是同一张,只是压缩或格式不同 | 自动合并,保留分辨率高的 |
| 3–6 | 同源变体:描边、单色、加文字 | 进人工队列,确认是版本还是重复 |
| 7–10 | 可能同校不同时期版本 | 人工确认后同组存多版本 |
| ≥11 | 判为不同学校 | 直接放过 |
坑在两个地方。一是纯色块多、线条极简的徽标 pHash 容易碰撞,所以用 pHash + dHash 双票表决;二是这段代码是 O(n²),几百张无感,上千张就该先按 phash 的高 16 位分桶,只在桶内两两比较,复杂度立刻降一个量级。
3.4 人工复核队列怎么建
把候选对导成 CSV,一行一对,附上两张图的路径和距离,审的人只需要在最后一列填merge、variant或keep,回读后再执行合并或分组。
| 列名 | 含义 |
|---|---|
a_path/b_path | 候选图片路径 |
p_dist/d_dist | 两个哈希的距离 |
name_a/name_b | 当前对齐到的校名,冲突时是重要线索 |
action | 人工填写:merge / variant / keep |
对齐到不同校名但距离 ≤ 2 的,基本都是文档里有重复条目,这类直接判 merge 不用犹豫。
提示:素材多来自公开渠道,商用前请自行确认授权范围,内部素材库和对外发布建议分开存放。
4. 建一个能按校名和按图两条路查的索引
4.1 SQLite 表结构与索引
单机、几千到几十万条,SQLite 完全够用,一个文件带走,不用起服务。
PRAGMA journal_mode = WAL; CREATE TABLE schools ( id INTEGER PRIMARY KEY, name_zh TEXT NOT NULL UNIQUE, -- 中文全称 slug TEXT NOT NULL UNIQUE, aliases TEXT DEFAULT '', -- 北大|PKU|Peking University emblem_path TEXT, logo_path TEXT, phash INTEGER, -- 64bit 哈希转有符号整数存储 sha256 TEXT, updated_at TEXT ); CREATE INDEX idx_schools_phash ON schools(phash); CREATE INDEX idx_schools_slug ON schools(slug); CREATE VIRTUAL TABLE schools_fts USING fts5( name_zh, aliases, content='schools', content_rowid='id' );FTS5 默认分词器不切中文,这是最容易踩的坑:不额外处理,搜「北京」匹配不到「北京大学」。常见做法是入库前把中文名按单字拆开、别名按空格拆开,和原串一起写进索引列。
def to_fts_tokens(name_zh, aliases): # 原串 + 单字切分 + 别名,兼顾"北京大学"整串命中与"北大"简称命中 zh = ' '.join(list(name_zh)) al = ' '.join(a.strip() for a in aliases.split('|') if a.strip()) return f'{name_zh} {zh} {al}'如果当前 SQLite 编译时没带 FTS5(PRAGMA compile_options;里看不到 ENABLE_FTS5),退回到普通表加LIKE '%关键词%'也能用,几千条数据下延迟完全感知不到,只是没法做相关度排序。
4.2 按名查询:三路召回
查询顺序按可靠度排:中文精确匹配 → 别名包含 → 前缀模糊。写成一条 SQL,用 UNION ALL 保持优先级。
SELECT id, name_zh, emblem_path, logo_path, 1 AS rank FROM schools WHERE name_zh = :q UNION ALL SELECT id, name_zh, emblem_path, logo_path, 2 AS rank FROM schools WHERE aliases LIKE '%' || :q || '%' UNION ALL SELECT id, name_zh, emblem_path, logo_path, 3 AS rank FROM schools WHERE slug LIKE :q || '%' ORDER BY rank, id LIMIT 20;rank越小优先级越高,前端按顺序展示即可。:q是绑定参数,不要用字符串拼接,中文简称里带引号的场景虽然少,但注入风险没必要留。
4.3 按图查询:汉明距离全表扫描
手上只有一张来路不明的校徽,要反查是哪所学校,就是把库里的 phash 逐个和查询哈希做异或再数 1 的个数。
import numpy as np, sqlite3, imagehash from PIL import Image def find_by_image(db, img_path, topk=5): q = int(str(imagehash.phash(Image.open(img_path).convert('L'), 8)), 16) con = sqlite3.connect(db) rows = con.execute( 'SELECT id, name_zh, emblem_path, phash FROM schools WHERE phash IS NOT NULL' ).fetchall() arr = np.array([r[3] for r in rows], dtype=np.uint64) dist = np.unpackbits((arr ^ np.uint64(q)).view(np.uint8) ).reshape(-1, 64).sum(axis=1) # 统计异或结果里 1 的个数 order = np.argsort(dist)[:topk] return [(rows[i][1], rows[i][2], int(dist[i])) for i in order]这段用unpackbits做位计数,不依赖 numpy 新版本的位计数 API,兼容性好;view(np.uint8)把 64 位异或结果按字节铺开,unpackbits再展开成 0/1,求和就是汉明距离。
| 记录规模 | 检索方案 | 单次查询量级 |
|---|---|---|
| 1 万以内 | numpy 全表异或 | 毫秒 |
| 1 万–50 万 | 按 phash 高 16 位分桶,桶内扫描 | 毫秒 |
| 50 万以上 | BK-tree / VP-tree,或交给向量检索 | 个位数毫秒 |
返回的距离值本身也是判断依据:距离 ≤ 6 可以直接信,7–12 建议在界面上给出 Top5 让人挑,超过 12 基本就是库里没有这张图。
4.4 返回结构怎么定
接口层建议把两个通道的返回结构统一,前端换检索方式不用改渲染逻辑。
{ "query_type": "image", "matches": [ {"name_zh": "北京大学", "slug": "beijingdaxue", "emblem": "emblem/beijingdaxue.png", "logo": "logo/beijingdaxue.png", "distance": 2, "confidence": "high"} ] }confidence由距离分段映射:0–6 给 high,7–12 给 medium 并强制返回 Top5,12 以上给 low 或直接空结果。前端拿confidence决定是直接展示还是弹个选择框。
5. 增量更新、校验与一条跑得住的流水线
5.1 用 sha256 做增量,别每次全量重跑
新收到一份「更为最全」的文档时,最怕的是全量重来一遍、把人工复核的结果冲掉。用内容哈希做去重和增量插入,能保住已经确认的数据。
import hashlib, pathlib def sha256_of(p): h = hashlib.sha256() with open(p, 'rb') as f: for chunk in iter(lambda: f.read(1 << 20), b''): h.update(chunk) return h.hexdigest() def diff_against_manifest(files, manifest): # manifest: {sha256: slug} new = [] for p in map(pathlib.Path, files): d = sha256_of(p) if d not in manifest: new.append((p, d)) return new1 << 20是 1MB 分块,避免把大图整个读进内存;只要字节完全一致就命中,比 pHash 更严格,专门用来判断「这批图有没有新增文件」。pHash 负责判断「新增的图是不是同一张校徽的另一种版本」,两者分工不重叠。流水线顺序是:sha256 过滤已见文件 → 对新增图跑 pHash 比对 → 命中已有条目则进人工队列,未命中则进新条目队列。
5.2 每次发布前跑的五项校验
| 校验项 | 判断方式 | 不通过的处理 |
|---|---|---|
| 主键唯一 | name_zh重复计数为 0 | 合并重复行,保留 phash 更小的 |
| 文件存在 | emblem_path逐个Path.exists() | 从 manifest 反查原始图片补回 |
| 尺寸统一 | 长宽均为 512 或 2048 | 重跑 normalize |
| 哈希完整 | phash IS NOT NULL占比 100% | 补算缺失项 |
| 透明通道 | 透明底版本 alpha 通道存在 | 改用白底版本兜底 |
这五项跑成一个脚本挂在发布命令上,出错就非零退出,比人工抽查靠谱得多。第二项尤其重要:改名或移动目录后路径会静默失效,等到前端报 404 才发现,返工成本很高。
5.3 一个具体技巧:标志图和校徽分两层存放
标题里写的是「标志图+校徽」,这两类东西其实不是一回事:校徽是纯图形徽标,多为圆形或盾形;标志图是带校名文字的横版组合标志。混在一个目录里,调用方永远搞不清该拿哪张。分两层存emblem/和logo/,表里也分成两个字段,检索接口默认返回 emblem,参数kind=logo时才返回组合标志。
入库时可以加一条粗略的自动判断:宽高比大于 1.6,或者图片底部五分之一区域的墨迹密度明显高于上部,就判为组合标志,先归到logo/并标记为待确认。误判率大概在两三成,但比全量人工分类快得多,剩下的人工只处理标记过的行。把这些校验和分层约定落到一条make release里,下次再收到一份新的「最全」文档,要做的就只是把文件丢进raw/,然后重跑一次。
本文还有配套的精品资源,点击获取