ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

校徽大全.doc转图片资产库:批量提取、pHash去重与SQLite检索

校徽大全.doc转图片资产库:批量提取、pHash去重与SQLite检索 简介一份汇集全国众多高校标志图与校徽的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.docWordDocument 流内嵌LibreOffice 转 docx 后再拆包OOXML.docxword/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:UserInstallationfile:///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(parentsTrue, exist_okTrue) 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, encodingutf-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, size512, bg(255, 255, 255, 0), tol12): 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, optimizeTrue)参数含义size512是展示与体积的折中做印刷素材另存一份 2048 的原图tol12是 RGB 差值阈值扫描件或带纹底的图噪点多调到 24~32 更稳0.94留安全边避免圆形校徽顶到画布边缘被裁。透明底和白色底建议各出一份PPT 场景白底更好用深色背景海报用透明底。3.2 命名规范与别名表统一命名是后面所有查询的基础。主键用中文全称文件名用 slug两个都不能省。字段示例说明name_zh北京大学主键语义唯一slugbeijingdaxue拼音全拼做文件名与 URLaliases北大|PKU|Peking University竖线分隔覆盖简称与英文名emblem_pathemblem/beijingdaxue.png纯图形徽标logo_pathlogo/beijingdaxue.png带校名的组合标志phash64 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_size8), imagehash.dhash(g, hash_size8) def dedupe(paths, thresh6): 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, contentschools, content_rowidid );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 编译时没带 FTS5PRAGMA 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, topk5): 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], dtypenp.uint64) dist np.unpackbits((arr ^ np.uint64(q)).view(np.uint8) ).reshape(-1, 64).sum(axis1) # 统计异或结果里 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 给 high7–12 给 medium 并强制返回 Top512 以上给 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参数kindlogo时才返回组合标志。入库时可以加一条粗略的自动判断宽高比大于 1.6或者图片底部五分之一区域的墨迹密度明显高于上部就判为组合标志先归到logo/并标记为待确认。误判率大概在两三成但比全量人工分类快得多剩下的人工只处理标记过的行。把这些校验和分层约定落到一条make release里下次再收到一份新的「最全」文档要做的就只是把文件丢进raw/然后重跑一次。本文还有配套的精品资源点击获取
返回列表