
长卷里的人都“活”了但我们需要换一个角度看它。当《仇英版清明上河图》被当作一幅描绘明代市井生活的图像大全时很多人的第一反应是感叹画面精美、人物众多。可如果你是一个做图像处理、数据可视化或者数字人文研究的技术人你大概率会冒出另一个问题这十米长卷里画了2200多个人人物姿态、行当全不重样能不能用工程化方法把它们“数清楚、分好类、画成图”这就是本文想展开的话题以仇英《清明上河图》的数字化分析为场景梳理一套从图像采集、人物标注、姿态与行当分类到可视化呈现的完整实操方案。我们不会只聊艺术史而是把这张画当作一个“高密度目标检测数据集”站在计算机视觉和数据分析的角度拆解它的信息量。整个过程涉及 OpenCV 目标检测、分类标签体系设计、长卷切图策略、标注格式转换和数据可视化适合正在做图像处理项目、数字人文研究或者单纯想用技术方式“围观”古画细节的开发者。1. 画作背景与数字化分析的切入点1.1 为什么是仇英版《清明上河图》提到《清明上河图》大部分人先想到的是北宋张择端的版本。那幅画以汴京郊野到市区的景象为线索描绘了北宋都城的生活百态。而仇英版本的《清明上河图》虽然沿用同名描绘的却是明代苏州城的繁华场景整体风格更偏向明代中后期的江南市井生活虹桥、城楼、商铺、戏台、婚嫁队伍、各式手工艺人内容比张择端版本更强调“世俗热闹”。更关键的一点在于仇英版在人物数量和细节密度上极有代表性。十米左右的绢本长卷上密布人物有说法称全卷人物超过两千人而且姿态各异、行当丰富。有人赶车、有人挑担、有人对弈、有人看戏、有人买卖、有人闲聊几乎每个人物都带着独立的动作信息。这个特征让它在图像分析里成了一个极有价值的研究对象画面信息密度高、人物重叠多、目标尺度差异大、场景纹理复杂。1.2 从图像研究者的角度看这张画如果我们把《仇英版清明上河图》看作一个“数据源”它有几点很值得注意图像尺寸极大十米长卷即使按 300 DPI 扫描也会得到一张横向像素可能超过 100000 像素的巨图。人物目标非常多而且大量人物只有几十到上百像素高属于典型的极小目标检测场景。长卷是连续场景光照相对统一但建筑、树木、桥梁等背景会对人物检测产生干扰。人物衣冠、姿态差异明显区分行当需要结合上下文场景信息而不仅仅是人形框。因此数字化分析这幅画不能只靠传统的目标检测模型“拉个框”还需要配合合理的图块切分、标注规范和领域知识。1.3 文章将要实现的整体流程本文的实战流程可以概括为从公开高清图源获取画作扫描图。将长卷切成适合模型推理的图块tile。使用目标检测模型或人工标注方式给每个人物生成边界框。建立行当、姿态、动作的标签体系。对标注结果做数据清洗和统计。用 Python 做可视化输出人物分布、行当构成、动作类型等结论。下面会逐步给出每个环节的可运行示例重点讲清楚为什么要这么设计。2. 环境准备与数据说明2.1 技术栈与版本思路为了避免版本兼容问题本文不锁定精确版本号而是给出一个经过验证的组合思路。你可以根据自己的实际环境安装下面列出的是相对稳定的选型方向Python 3.9 或更高版本。OpenCV用于图像读取、切块、缩放和基础处理。PyTorch如果用现代目标检测模型需要配套的深度学习框架。LabelImg 或 X-AnyLabeling用于人工标注生成 PASCAL VOC 或 COCO 格式。Pandas、Matplotlib、Seaborn用于统计分析和可视化。NumPy数组计算。Pillow处理超大图的另一种选择。2.2 数据集说明本文不直接提供画作高清扫描文件因为版权和来源需要授权。建议读者选用博物馆公开的数字资源比如台北故宫博物院 Open Data 专区或合法开放的在线艺术档案。代码示例中以一个模拟的文件夹结构为例qingming_chouying/ ├── raw/ │ ├── tile_0001.jpg │ ├── tile_0002.jpg │ └── ... ├── annotations/ │ ├── coco_total.json │ └── labels.txt ├── models/ │ └── README.md ├── scripts/ │ ├── split_long_image.py │ ├── detect_person.py │ ├── merge_results.py │ └── visualize_stats.py └── output/分块的原因是深度学习模型通常无法直接处理几万像素宽的整张长图即便能处理计算量也大到不现实。所以切块策略是整个流程的第一步。3. 核心思路超长画的切块与坐标还原3.1 为什么要切块一个正常的检测模型输入尺寸通常为 640×640 或 1280×1280。如果把整张十米长卷直接塞进去图像会被严重压缩人物细节丢失检测结果几乎没有参考价值。就算使用大模型GPU 显存也可能不够。切块之后需要注意坐标还原模型在小图上检测到目标的坐标需要映射回原长卷坐标否则无法统计全局分布。3.2 滑窗切块实现下面是一个基于 OpenCV 的长卷切块脚本。它会按指定步长滑动裁剪并把每个图块的原点坐标写入文件名方便后续还原。import cv2 import os def split_long_image(image_path, output_dir, tile_size1280, overlap200): 将长卷图像切分为多个图块 :param image_path: 原图路径 :param output_dir: 输出目录 :param tile_size: 图块边长 :param overlap: 重叠像素数保证跨块目标不丢失 os.makedirs(output_dir, exist_okTrue) img cv2.imread(image_path) if img is None: print(f无法读取图像: {image_path}) return h, w img.shape[:2] step tile_size - overlap tile_index 0 for y in range(0, h, step): for x in range(0, w, step): x_end min(x tile_size, w) y_end min(y tile_size, h) # 边界块做填充避免右侧和下侧出现窄条 tile img[y:y_end, x:x_end] tile_h, tile_w tile.shape[:2] if tile_h tile_size or tile_w tile_size: padded np.zeros((tile_size, tile_size, 3), dtypenp.uint8) padded[:tile_h, :tile_w] tile tile padded # 文件名记录原点坐标tile_y_x.jpg out_name ftile_{y:05d}_{x:05d}.jpg cv2.imwrite(os.path.join(output_dir, out_name), tile) tile_index 1 print(f切块完成共生成 {tile_index} 个图块)3.3 坐标映射回原图如果模型在第 i 个图块检测到某个人物边界框为(x_tl, y_tl, x_br, y_br)加上该图块在整图中的原点偏移量就能还原回原图坐标def map_to_global(tile_x, tile_y, box, tile_size1280, overlap200): box: [x_tl, y_tl, x_br, y_br] 模型输出的像素坐标 tile_x, tile_y 来自图块文件名中的坐标 global_x1 tile_x box[0] global_y1 tile_y box[1] global_x2 tile_x box[2] global_y2 tile_y box[3] return [global_x1, global_y1, global_x2, global_y2]这一步看起来简单却是后续统计、可视化正确性的基础。如果坐标映射出错后面画出来的长卷人物分布图就会错位所有的分组统计也就失去了意义。4. 建立人物形态与行当标签体系4.1 标签体系设计原则做古画人物分析不能只检测“是不是人”还要记录“这个人是什么状态”。不同项目目标不同标签体系设计也不一样。针对《仇英版清明上河图》建议按三个维度打标人物密度维单个人、群组、聚众场景。姿态动作维站立、行走、挑担、推车、骑行、对弈、跪拜、作揖、观望、劳动等。行当身份维商贩、轿夫、渔夫、工匠、书生、官员、僧侣、乐手、杂役、妇人、孩童等。4.2 分类标签示例person person_walking person_standing person_sitting person_carrying_pole person_pushing_cart person_riding_horse person_watching_show person_trading person_bowing person_rowing ...这里不建议只打“人”一个类别。虽然检测模型只需要输出person但后续想要回答“明代苏州街头的常见行当是什么”必须依赖更丰富的语义标签。所以实际操作中通常分层处理先用目标检测模型或人工框选得到人物位置框。再对每个框做 image classification 分类判断姿态与动作。结合周围场景判断行当属性。4.3 姿态判断的可视化思路对于一张古画区域可以截取检测到的单个人物框然后保存为一个独立的小图再批量给这些“人物小图”打上姿态分类标签。这样可以把标注任务拆成两个阶段降低单次标注难度。import cv2 def crop_person_boxes(raw_image_path, boxes, output_dir): boxes: 原图坐标列表每个元素是 [x1, y1, x2, y2] img cv2.imread(raw_image_path) os.makedirs(output_dir, exist_okTrue) for idx, box in enumerate(boxes): x1, y1, x2, y2 [int(v) for v in box] # 扩展一点边界保留衣物轮廓 pad 10 x1 max(0, x1 - pad) y1 max(0, y1 - pad) x2 min(img.shape[1], x2 pad) y2 min(img.shape[0], y2 pad) person_crop img[y1:y2, x1:x2] if person_crop.size 0: continue out_path os.path.join(output_dir, fperson_{idx:05d}.jpg) cv2.imwrite(out_path, person_crop) print(f已裁剪 {len(boxes)} 个人物区域)这样的“检测 裁剪 再分类”流程在人物密集场景中非常实用。因为检测框可能不够精确先裁出来让人工或小分类模型判断会比直接在大图上标注容易很多。5. 基于目标检测模型识别长卷人物5.1 模型选择建议对于这幅长卷中大量小尺寸人物YOLOv8 或 RT-DETR 这类模型都有较好的效果。如果硬件资源有限可以采用 YOLOv8n 这样的小模型然后在自有标注数据上微调。不过这里需要明确直接用预训练模型识别古画人物效果通常不太理想因为古画的绘画风格和真实照片差异很大。理想情况是先人工标注一部分人物区域比如 500~1000 个框。用标注数据微调模型。再对全图做推理。人工检查漏检和误检。如果只是做快速浏览也可以先用预训练模型跑一遍把检测结果人工修正后当作基础标注。5.2 基于 YOLOv8 的检测示例from ultralytics import YOLO # 加载模型这里假设已经经过微调 model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcetiles, conf0.25, saveFalse, imgsz1280, devicecuda )跑完后需要把结果按照前面提到的坐标映射方式还原到原图。代码可以这样写import os all_boxes [] for result in results: tile_path result.path filename os.path.basename(tile_path) # 从文件名解析原点坐标 # tile_y_x.jpg parts filename.replace(.jpg, ).split(_) tile_y int(parts[1]) tile_x int(parts[2]) boxes result.boxes.xyxy.cpu().numpy() for box in boxes: x1, y1, x2, y2 box # 去掉 padding 区域影响 global_box map_to_global(tile_x, tile_y, [x1, y1, x2, y2]) all_boxes.append(global_box)这里需要注意如果切块时做了边界填充那么落在填充区域的检测结果需要特殊处理否则会产生靠近右边界和下边界的重复检测。一种简单做法是如果检测框中心点落在 padding 区就跳过。5.3 重叠区去重滑窗切块会带来大量重叠区域同一个目标可能出现在多个相邻图块中因此需要进行 NMS非极大值抑制或自定义去重。直接用 NMS 的简单方法import numpy as np def nms_boxes(boxes, iou_threshold0.5): if len(boxes) 0: return [] boxes np.array(boxes).astype(float) x1 boxes[:, 0] y1 boxes[:, 1] x2 boxes[:, 2] y2 boxes[:, 3] areas (x2 - x1) * (y2 - y1) order areas.argsort()[::-1] keep [] while order.size 0: i order[0] keep.append(i) xx1 np.maximum(x1[i], x1[order[1:]]) yy1 np.maximum(y1[i], y1[order[1:]]) xx2 np.minimum(x2[i], x2[order[1:]]) yy2 np.minimum(y2[i], y2[order[1:]]) w np.maximum(0.0, xx2 - xx1) h np.maximum(0.0, yy2 - yy1) inter w * h iou inter / (areas[i] areas[order[1:]] - inter) inds np.where(iou iou_threshold)[0] order order[inds 1] return [boxes[int(i)] for i in keep]在原场景中如果两个“人物”在画面里确实紧挨着当前后景人物重合度较高时NMS 可能会误删其中一个。因此阈值不能设得太低建议在 0.4~0.6 之间根据实际效果调整。6. 数据统计与可视化6.1 把检测结果整理成 DataFrame在分析和可视化之前最好把所有检测框和标签整理成结构化表格。例如import pandas as pd columns [x1, y1, x2, y2, width, height, label] data [] for box, label in zip(all_boxes, all_labels): x1, y1, x2, y2 box data.append([x1, y1, x2, y2, x2 - x1, y2 - y1, label]) df pd.DataFrame(data, columnscolumns) df[center_x] (df[x1] df[x2]) / 2 df[center_y] (df[y1] df[y2]) / 2 df.to_csv(output/person_stats.csv, indexFalse)有了这个表格就可以回答很多问题。6.2 绘制长卷人物分布热力图《仇英版清明上河图》最有趣的特征是“哪里人多、哪里人少”把人物中心点投射在长卷横向位置上就能看出来画面节奏。我们以中心点 x 坐标作为横轴统计不同区段的人物数量import matplotlib.pyplot as plt import numpy as np plt.figure(figsize(20, 4)) plt.hist(df[center_x], bins200, color#8B0000, alpha0.8) plt.title(仇英清明上河图人物横向分布) plt.xlabel(长卷横向位置像素) plt.ylabel(人物数量) plt.show()6.3 行当构成统计如果每个检测框已经标注了行当类型可以使用 Pandas 做分组统计category_count df[label].value_counts() print(category_count) plt.figure(figsize(12, 4)) category_count.plot(kindbar, color#2E8B57) plt.title(人物行当统计 Top 20) plt.ylabel(数量) plt.show()除了这些基础图还可以继续做人物身高分布图。不同场景区段的行当交叉统计。人物朝向的极坐标图。画面关键区域桥、城门、戏台的人群密度对比。这些图可以帮助研究者更直观地理解画面结构也为后续写论文或做策展提供数据支撑。7. 标注工具与人工复核流程7.1 推荐标注工具人工标注古画人物最常见的选择有两个LabelImg轻量级适合快速画矩形框支持 VOC 格式。X-AnyLabeling支持交互式分割和自动标记处理重叠目标效率更高。对于长卷中大量小目标建议配合“自动检测 人工修正”的工作流先用模型或预标注系统生成一批候选框再让标注人员删除误检、补充漏检。这样能极大降低工作量。7.2 标注质量检查标注质量直接决定模型能力和统计结论的可信度。建议至少做以下检查框是否贴合人物主体有没有把背景大范围框进去。是否存在重复框特别是两个目标重叠的时候。是否存在漏检尤其要注意暗部、树荫下、建筑转角处的小人物。类别标签是否统一避免出现“商人”和“商贩”混用的情况。这里也建议在标注规范里写清楚“边界不清晰时如何处理”。古画是写意风格人物轮廓往往不是清晰轮廓线标注员之间需要形成一致约定。8. 常见问题与排查思路在长卷人物的数字化分析中新手最容易遇到下面几类问题这里整理成了一个排查表格。问题现象常见原因解决思路切块后文件太多磁盘空间不足图块保存为无损 JPG或步长太小导致重叠过多提高步长压缩比或使用 PNG 转 JPG也可以直接不落盘在内存中推理模型识别出大量非人物目标预训练模型没见过古画风格人工标注部分数据后微调模型或使用风格迁移数据增强跨块目标被重复计数重叠区域没有去重合并结果后用 NMS 或按中心点去重检测框坐标错位未做坐标原点映射检查文件名解析逻辑确保 tile_y、tile_x 顺序正确人物密集区域漏检严重目标太小或相互遮挡提高推理图像分辨率或把大图块再细分使用小目标增强策略统计人数与视觉感受相差太大漏检误检同时存在先做抽样人工审核计算精确率和召回率再决定阈值与模型调整方向手工标注花费时间过长标注工具使用不熟练或规范不清晰先标注少量样本统一标准再用预标注辅助9. 工程项目落地中的最佳实践如果要把这张古画的数字化分析做成一个相对正式的研究项目而不只是个人练手有几个工程问题值得提前规划。9.1 统一坐标系与切图规范所有图块必须保持统一的命名规范和坐标记录方式。建议在图块文件名中直接编码原点坐标而不是额外维护一份映射表。虽然映射表看起来更灵活但工程里文件名丢失、排序错误都会造成坐标错乱。用tile_{y}_{x}.jpg的方式更容易排查问题。9.2 训练集与测试集切分如果最后要训练模型不能把同一个人物的重叠块同时放进训练集和验证集。因为滑窗产生的相邻图块内容高度重叠会让模型评估指标虚高。建议按照场景切分不让同一场景区域同时出现在训练和验证中。9.3 标注一致性多个标注人员参与时要考虑标签规范的细化程度。比如“推车”和“拉车”需要明确车轮相对人的位置“作揖”和“拱手”在画面里很难区分是否合并为一个类别要提前定义。必要时可以用标注示例图来说明。9.4 数字人文研究的可解释性如果这篇文章的内容服务于学术研究建议把检测和分类结果做成可回溯的版本。例如导出一张带标注框的低分辨率整图快速核对某个统计数据背后对应的是画面中的哪些人物。这样能避免数据漂亮但无法经受仔细推敲的尴尬。10. 下一步可以怎么做仇英《清明上河图》之所以被视为一部“明代生活图像大全”是因为它的细节不是个别亮点而是成体系的信息密度。通过目标检测、姿态分类、空间统计和可视化我们完全可以把它拆成一组可量化的文化数据市井职业的组成比例、聚众场景的空间位置、人物动态的密集区间。如果你对这个方向感兴趣可以做三件事找一张高清古画长卷先完成切块、预标注、人工修正的完整流程跑通以后再扩展到更多画作。试着用分类模型对每个人物区域做姿态识别比较不同姿态类别在画面不同位置的分布变化。如果对数字人文感兴趣可以把画作中的空间关系和历史文献对应起来形成跨模态的研究素材。技术不是用来“解构”一幅画的美感而是帮助我们看到肉眼容易忽略的群体性规律。人物虽多放大之后各有各的姿势与营生缩到统计视图里又能呈现出明代市井的空间结构。这种“看画”的方式对做文化遗产数字化的开发者来说本身就是一个有趣的长期课题。希望这篇教程能帮你打开思路跑通属于自己的古画分析流程。