用Python+OpenCV+KMeans把植物园照片变成颜色地图
2026/9/3 12:14:22 网站建设 项目流程

开头我要先澄清一点:这篇文章并不是一篇石家庄植物园的游记。虽然标题看起来很像,但我真正想聊的是“怎么把植物园变成一整套颜色数据”。逛植物园时,我们习惯用“植物名 + 观赏月份 + 分区位置”来记录;如果换一个维度,用“颜色”去重新组织这些照片,会得到一种类似专业设计师取色的体验。我们可以把石家庄植物园理解成一个样本库,每张照片都是一个颜色采集样本,然后通过 Python + OpenCV + KMeans,把照片里的花、叶、树、草地压缩成一组色卡,再按拍摄位置绘制成一张颜色地图。

这里有一个明确判断:做这类项目,真正难的不是“会不会写 KMeans 聚类代码”,而是如何控制输入图片质量,以及如何在颜色提取前把背景、阴影、天空、游客这些干扰去掉。很多初学者会把全部精力放在调参上,结果换一张照片就失效。本文会把整个流程拆开:从拍照建议、颜色空间原理、掩码分割、主色提取,到批量入库和可视化,给出可复制的代码思路。也就是说,读完这篇,你不仅能处理石家庄植物园的照片,也能把这套流程迁移到其他园区、城市街道甚至自己的相册颜色分析里去。

我要清楚交代适用范围:适合熟悉 Python 基础、正在学图像处理或想做一个完整数据可视化小项目的读者。不需要深度学习基础,也不需要企业级算力,一台普通电脑足够。

1. 为什么要用“颜色”来打开植物园

通常大家逛植物园,默认的信息组织方式是按物种:这是月季,那是荷花,春有樱花,秋有银杏。这种组织方式科学,但有一个问题:它没办法回答一个很感性的问题——“从整体上看,今天的植物园是什么色调?”

如果只是拍一些照片,发到朋友圈,问题不等于解决。因为照片之间的颜色没有关联,也没有空间位置。看到一张郁金香的照片,你只知道“这里有一片郁金香”,不知道它在该园区整体的颜色分布里处于什么位置。假如我们把每张照片当成一个调查样本,提取出其中的代表色,再把颜色和拍摄位置绑定,得到的就是一张“园区色彩分布图”。

这种数据的价值不只是有趣。做城市景观规划的人,可以分析植物园不同季节的季相颜色变化;做 UI 设计、插画、影视美术的人,可以从真实场景中收集自然配色方案;做旅游内容的人,可以把“植物园的秋季是橙色系还是红色系”这种问题变成可以验证的数据结论。

从实现层面看,这个项目也很有意思。它同时涉及图像读取、颜色空间、聚类算法、数据管理和可视化,但整体难度又处在“一个人能在周末跑通”的范围。它比单纯练算法更有完成感,因为最终输出是一个可以给别人讲解的成果物:一张颜色地图、一份色卡 JSON,或者一组不同展区的对比调色板。

如果只把目光放在调参和代码上,很容易错过真正有价值的工程点。其实“把颜色变成数据”这件事,一大半工作在拍照和预处理环节。单张照片受到天气、时间、逆光、叶片反光的影响,颜色记录并不稳定。想得到可比较的颜色数据,必须在采集阶段就做好控制,在后期用颜色空间转换和掩码尽量去掉非植物区域。这是后面所有分析的基础。

2. 颜色提取的核心概念与适用场景

开始写代码前,有几个基础概念必须先弄清楚。很多颜色分析结果不理想,不是代码写得不对,而是没有理解颜色空间和分割方式。

2.1 RGB、HSV 和 Lab,到底该用哪个

图像在 OpenCV 中默认以 BGR 顺序保存,本质是 RGB 颜色空间的一种排列方式。RGB 用红、绿、蓝三个分量表示颜色,优点是显示设备直接使用,缺点是不符合人对颜色的感知习惯。比如深绿和浅绿在 RGB 里距离很大,但在人眼里都是“绿”。如果直接用 RGB 的欧氏距离判断两个颜色是否相似,经常会把视觉上接近的颜色分成两类,又把光照造成的明暗变化误判成不同颜色。

HSV 把颜色拆成色相、饱和度和明度。色相接近 0 或 180 的是红色系,接近 60 的是黄色系,接近 120 的是绿色系。这样的表达方式更适合做颜色阈值,例如“只保留绿色植物区域”“只保留高饱和度的花朵区域”。缺陷是 HSV 的色相在低饱和度时不稳定,灰色区域的色相很容易跳动。

Lab 是一种感知均匀的颜色空间,L 表示亮度,a 表示从绿色到红色的分量,b 表示从蓝色到黄色的分量。Lab 中两个颜色的欧氏距离与人眼感知更接近。做颜色聚类和颜色差异判断时,Lab 通常比 RGB 更准确。

实际项目中,我建议这样组合使用:用 HSV 做快速前景掩码,用 Lab 或 RGB 做聚类输入。如果只是提取颜色,RGB 聚类也能用;但如果涉及颜色相似度比较、不同季节颜色差异计算,建议统一转成 Lab。

颜色空间主要特点典型用途
RGB / BGR直观、和显示设备一致读图、显示、保存结果
HSV色相、饱和度、明度分离按颜色区域做阈值掩码
Lab感知相对均匀颜色距离、颜色差异分析

2.2 KMeans 怎么从照片中提取色卡

KMeans 本身是无监督聚类算法,核心思路是把数据分成 K 组,使组内样本到中心点的距离之和尽量小。应用到颜色提取时,像素不是“几何位置”,而是颜色空间中的点。一张 400 万像素的照片,就是 400 万个三维颜色点。KMeans 要做的,是把这些颜色点聚成 K 类,每一类的中心点就是调色板中的一种代表色。

这里有一个新手容易踩的坑:KMeans 聚类时默认按像素数量投票,不能直接表达“视觉重要性”。一片大面积的绿叶在一张照片中可能占了 90% 的像素,如果 K 设成 3,聚类结果很可能是“三种深浅接近的绿色”,画面中央一朵小红花反而被忽略。如果你要分析的是花色而不是草地,不能简单做全图聚类,而要先构造掩码,把目标区域分离出来,再对目标区域中的像素做聚类。

2.3 掩码是什么,它为什么如此重要

掩码是跟原图尺寸相同的一幅黑白图,白色像素表示需要参与计算,黑色像素表示丢弃。通过 HSV 阈值可以得到“绿色掩码”;通过 HSV 中红色、粉色、黄橙色的组合范围可以得到“花区域掩码”。常见的形态学操作也很关键,例如使用开运算先腐蚀再膨胀,可以去除零散噪点;使用闭运算先膨胀再腐蚀,可以把花瓣内部的小空洞补上。

如果拍摄对象是一棵树、一片草地,简单的绿色掩码通常够用。如果拍摄对象是单株花卉,周围有杂草、泥土、其他植物,阈值方法容易混入干扰颜色,这时可以考虑 OpenCV 的 GrabCut 算法。GrabCut 会根据前景色与背景色之间的概率模型做分割,对复杂背景更友好,代价是计算量更高。小图可以先缩放到 500 到 700 像素边长再运行,速度能够接受。

这套思路用在石家庄植物园时要特别注意:植物园里并不是所有区域都是大面积绿色。花境里不同花卉的颜色密集排列,阴影中叶子会变成深褐色或墨绿色,反光叶片又有明显高光。一种固定的绿色阈值不可能覆盖所有情况,因此工程上要做的是“多条件组合 + 中心区域优先 + 形态学修复”,而不是迷信某一组数值能一劳永逸。

3. 项目设计与技术选型

整个项目可以抽象成一条线性流水线:照片采集、预处理、区域掩码、主色聚类、结果入库、空间可视化。如果用一句话总结:输入是一批照片,输出是一系列色卡和一张颜色地图。

3.1 推荐的目录结构

color-garden/ ├── data/ │ ├── raw/ # 原始照片,按日期或分区命名 │ └── metadata.csv # 照片与位置、场景类型的对应表 ├── output/ │ └── palettes.json # 提取结果 ├── scripts/ │ ├── color_extractor.py # 单图颜色提取 │ ├── run_pipeline.py # 批量处理入口 │ └── build_map.py # 生成颜色地图 └── requirements.txt

建议从一开始就保持照片和数据描述分离。颜色提取代码只需要读取data/raw/下文件,不需要知道照片属于哪个区域,但metadata.csv记录了每张照片在哪拍摄、是什么场景。这样后期想修改提取策略,不需要改数据表;想替换园区,也不需要改代码,只要换一批照片和一份新的 metadata。

3.2 技术选型原因

OpenCV 负责图像读写、颜色空间转换、掩码和形态学处理,是图像处理的主力。NumPy 负责数组操作和像素索引。Matplotlib 用来绘制色卡和颜色地图。Pandas 用来读取 metadata.csv。颜色聚类直接使用 OpenCV 内置的cv2.kmeans,不需要额外安装 scikit-learn,也可以减少依赖冲突。

Python 版本建议 3.9 或以上,但这不是硬性要求。OpenCV 版本使用 4.x 即可,代码里没有依赖某个远古 API。如果电脑上没有安装 OpenCV,环境准备阶段一行 pip 命令就能完成。

3.3 两种拍摄场景对应不同算法策略

需要区分大面积观赏区和单株植物。大面积观赏区,例如一大片郁金香、一片银杏林,可以全图或者大范围掩码后聚类,因为颜色主体相对统一。单株植物,例如一朵荷花特写,需要先分割花卉主体,否则小花在全局像素里占比太低,聚类出来就是绿色的茎和荷叶,而不是花朵颜色。

对应到 metadata.csv,可以增加一列scene_type,值设为areasingle。批量处理时,area走全图掩码聚类;single走中心区域 GrabCut 或更严格的花色掩码。这种设计比“一个函数处理所有图片”更贴近真实需求。

4. 环境准备与前置条件

主流程只需要 OpenCV、NumPy、Pandas、Matplotlib。建议创建一个虚拟环境,避免污染系统 Python。

mkdir color-garden && cd color-garden python -m venv venv source venv/bin/activate # Windows 下使用:venv\Scripts\activate pip install opencv-python numpy pandas matplotlib pillow

如果下载速度慢,可以使用国内镜像:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python numpy pandas matplotlib pillow

安装完成后,可以用一个小命令确认 OpenCV 正常导入:

python -c "import cv2; print(cv2.__version__)"

如果顺利打印出版本号,说明基础环境可用。下面准备示例数据文件。用石家庄植物园做真实项目时,先把现场拍的照片放到data/raw/,然后在data/metadata.csv里登记每张照片。metadata.csv的列设计如下:

file_name,area,x,y,scene_type,notes IMG_0001.jpg,入口花境,0.12,0.15,area,多云 IMG_0002.jpg,湖边草坪,0.20,0.68,area,顺光 IMG_0003.jpg,林荫道,0.47,0.42,single,拍摄单株

这里的xy表示拍摄位置在园区平面图上的相对坐标。如果不方便做地理配准,可以先把园区地图等比放到一张 1000×1000 的图上,手动标出拍摄点,再把坐标除以 1000,得到 0 到 1 之间的值。这个坐标不需要很精确,只要能够表示“不同拍摄点之间的空间关系”就够了。上表的区域名只是示例,实际项目请对你拍摄现场的展区或者地标位置。

5. 单张图像主色提取核心代码

先从单张图片开始。我建议不要一上来就写批量处理,而是先拿两三张照片跑通单图提取,确认输出和肉眼判断基本一致,再考虑扩展。

创建scripts/color_extractor.py

# scripts/color_extractor.py import json import argparse from pathlib import Path import cv2 import numpy as np def read_image(path: str, max_side: int = 720): """读取图片,并等比缩放到最长边不超过 max_side。""" img = cv2.imread(str(path)) if img is None: raise ValueError(f"无法读取图片: {path}") h, w = img.shape[:2] scale = max_side / max(h, w) if scale < 1: new_w = int(w * scale) new_h = int(h * scale) img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA) return img def build_plant_mask(img_bgr: np.ndarray) -> np.ndarray: """ 尽量保留植物区域,丢弃天空、道路、游客等干扰。 这里使用 HSV 阈值,并对结果做中心优先和形态学修复。 """ hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) # 绿色植物区域 green_low = np.array([35, 40, 40]) green_high = np.array([85, 255, 255]) green_mask = cv2.inRange(hsv, green_low, green_high) # 红花、粉花等常见花色区域 flower_low_1 = np.array([0, 60, 60]) flower_high_1 = np.array([15, 255, 255]) flower_low_2 = np.array([140, 60, 60]) flower_high_2 = np.array([179, 255, 255]) flower_mask = cv2.inRange(hsv, flower_low_1, flower_high_1) | cv2.inRange( hsv, flower_low_2, flower_high_2 ) mask = green_mask | flower_mask # 中心区域优先:假设主体位于画面中部 h, w = mask.shape center_mask = np.zeros((h, w), dtype=np.uint8) cx, cy = w // 2, h // 2 radius = int(min(w, h) * 0.35) cv2.circle(center_mask, (cx, cy), radius, 255, -1) mask = cv2.bitwise_and(mask, center_mask) # 形态学处理:去孤立噪点,补内部小洞 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return mask def bgr_to_hex(bgr) -> str: """OpenCV 默认 BGR 顺序,转成网页色号需要反过来。""" r, g, b = int(bgr[2]), int(bgr[1]), int(bgr[0]) return f"#{r:02X}{g:02X}{b:02X}" def extract_palette(image_path: str, k: int = 5): """提取图片中的 k 个代表色,返回按占比降序排列的色卡。""" img = read_image(image_path) mask = build_plant_mask(img) pixels = img[mask > 0] # 如果掩码后像素太少,说明主体可能不是常见植物颜色, # 回退到全图聚类,避免返回空结果。 if len(pixels) < k * 10: pixels = img.reshape(-1, 3) data = np.float32(pixels) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0) _, labels, centers = cv2.kmeans( data, k, None, criteria, 3, cv2.KMEANS_PP_CENTERS ) counts = np.bincount(labels.flatten(), minlength=k) ratios = counts / counts.sum() order = np.argsort(-ratios) palette = [] for idx in order: palette.append( { "hex": bgr_to_hex(centers[idx]), "bgr": [ int(centers[idx][0]), int(centers[idx][1]), int(centers[idx][2]), ], "ratio": round(float(ratios[idx]), 4), } ) return palette if __name__ == "__main__": parser = argparse.ArgumentParser(description="提取植物园照片主色") parser.add_argument("--image", required=True, help="待处理图片路径") parser.add_argument("--k", type=int, default=5, help="需要提取的颜色数量") args = parser.parse_args() result = extract_palette(args.image, k=args.k) print(json.dumps(result, ensure_ascii=False, indent=2))

这段代码有几个值得注意的设计点。

第一,read_image先把图片最长边缩放到 720 像素。这能显著减少 KMeans 的计算量,而且对颜色提取结果影响很小。原始图片 4000 万像素和缩放后的 50 万像素,在代表色上差别不大,但耗时差别巨大。

第二,build_plant_mask不只保留绿色,也保留红色和粉色区域。这是因为植物园的重要观赏对象是花,而花经常是红色或粉色。如果只保留绿色,后续聚类会丢掉很多有价值的信息。代码中的红粉色阈值覆盖了两个区间,因为红色在 HSV 色相环中跨越 0 度两侧。

第三,所有常量都集中在函数最前面。后续你拿着石家庄植物园不同场景的照片调试时,可能要反复调整绿色阈值、中心半径、形态学核大小。把这些参数放在显眼位置,是工程上非常关键的实践。

运行示例:

cd color-garden python scripts/color_extractor.py --image data/raw/IMG_0001.jpg --k 6

预期会打印一个 JSON 数组,数组里每个对象包含色号、BGR 值和该颜色占调色板的比例。真正的颜色值取决于照片内容,但输出结构应该如下:

[ { "hex": "#4A7A2F", "bgr": [47, 122, 74], "ratio": 0.3821 }, { "hex": "#9C4B31", "bgr": [49, 75, 156], "ratio": 0.2147 } ]

如果输出中某个颜色和肉眼观察严重不符,第一步应该输出中间掩码,用cv2.imwrite保存黑白掩码看看哪些区域被保留,哪些区域被误删。不要先怀疑聚类算法。图像处理项目里,掩码错误造成的问题远远多于 KMeans 参数问题。

6. 复杂背景下的掩码优化

上面的代码能够覆盖很多“主体居中、背景简单”的照片,但在真实植物园场景里,会遇到三类问题:阴影让绿色叶片变成深褐色;阳光直射让花瓣严重过曝;背景里的泥土、树干、天空和游客混入取样区域。

6.1 提高饱和度下限

第一种优化思路是调整 HSV 阈值。植物园的植物颜色通常饱和度和亮度都不低,但土地、水泥路面、树干偏灰。把 HSV 的饱和度下限从 0 提高到 40 或 60,有助于过滤掉灰色背景。不过,深色阴影区域的饱和度也会下降,阈值过高可能把背阴的叶片全部剔除。

# 示例:设置饱和度下限为 50 green_low = np.array([35, 50, 40]) flower_low_1 = np.array([0, 70, 60])

这组数值不是万能解。不同天气、不同时间段拍摄的照片,饱和度分布差异很大。更稳妥的做法是保存中间掩码,借助可视化工具反复观察,再针对自己的照片集微调。

6.2 使用 GrabCut 做单株主体分割

对于“单株花、背景杂乱”的照片,单纯阈值不够。OpenCV 的 GrabCut 只需要初始化一个矩形,就能自动估计前景和背景的分布。它对叶子边缘的花瓣形状处理较好,但计算量大于阈值。建议先缩放图片再执行。

def grabcut_foreground_mask(img_bgr: np.ndarray) -> np.ndarray: """ 基于矩形初始化的 GrabCut 前景分割。 返回 0/255 的二值掩码,255 表示前景。 """ h, w = img_bgr.shape[:2] init_mask = np.zeros((h, w), np.uint8) rect = (int(w * 0.05), int(h * 0.05), int(w * 0.9), int(h * 0.9)) bgd_model = np.zeros((1, 65), np.float64) fgd_model = np.zeros((1, 65), np.float64) cv2.grabCut( img_bgr, init_mask, rect, bgd_model, fgd_model, iter_count=5, mode=cv2.GC_INIT_WITH_RECT, ) mask = np.where( (init_mask == cv2.GC_FGD) | (init_mask == cv2.GC_PR_FGD), 255, 0, ).astype(np.uint8) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return mask

这个函数可以作为build_plant_mask的一种替代。在metadata.csv中标记single的照片走 GrabCut,标记area的照片走 HSV 阈值分割。注意cv2.grabCut会修改传入的init_maskbgd_modelfgd_model,所以每次调用需要重新创建这些变量,不能复用上一次调用后的模型数据。如果同一张图要跑多次,务必要重新初始化。

7. 批量生成植物园色彩库

单张照片的提取结果只能回答“一张图片是什么颜色”,拿不到空间分布信息。批量处理需要结合metadata.csv,为每张照片生成调色板,再输出到一个 JSON 文件。

创建scripts/run_pipeline.py

# scripts/run_pipeline.py import json from pathlib import Path import pandas as pd from color_extractor import extract_palette, grabcut_foreground_mask, read_image import cv2 def extract_palette_with_scene(image_path: str, scene_type: str, k: int = 5): """根据场景类型选择不同的掩码策略。""" if scene_type == "single": img = read_image(image_path, max_side=720) mask = grabcut_foreground_mask(img) pixels = img[mask > 0] if len(pixels) < k * 10: pixels = img.reshape(-1, 3) data = cv2.convertScaleAbs(pixels.astype(np.float32)) else: # area 类型继续使用通用 HSV 掩码 return extract_palette(image_path, k=k) from color_extractor import bgr_to_hex import numpy as np data = np.float32(data) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0) _, labels, centers = cv2.kmeans(data, k, None, criteria, 3, cv2.KMEANS_PP_CENTERS) counts = np.bincount(labels.flatten(), minlength=k) ratios = counts / counts.sum() order = np.argsort(-ratios) palette = [] for idx in order: palette.append( { "hex": bgr_to_hex(centers[idx]), "ratio": round(float(ratios[idx]), 4), } ) return palette def main(): project_root = Path(__file__).resolve().parent.parent metadata_path = project_root / "data" / "metadata.csv" raw_dir = project_root / "data" / "raw" output_path = project_root / "output" / "palettes.json" output_path.parent.mkdir(parents=True, exist_ok=True) df = pd.read_csv(metadata_path) results = {} for row in df.itertuples(index=False): image_file = row.file_name image_path = raw_dir / image_file if not image_path.exists(): print(f"跳过不存在的图片: {image_path}") continue try: palette = extract_palette_with_scene( str(image_path), scene_type=getattr(row, "scene_type", "area"), k=5, ) results[image_file] = { "area": row.area, "x": row.x, "y": row.y, "palette": palette, } print(f"处理成功: {image_file}") except Exception as exc: print(f"处理失败: {image_file}, 错误: {exc}") with open(output_path, "w", encoding="utf-8") as fp: json.dump(results, fp, ensure_ascii=False, indent=2) print(f"结果已保存到 {output_path}") if __name__ == "__main__": main()

这段代码展示了一个常见工程技巧:用metadata.csv驱动批量任务。metadata.csv里记录的scene_type决定走哪种掩码策略,让“大面积区域”和“单株植物”可以差异化管理。

有一个地方需要提前解释:我在run_pipeline.py中为了演示scene_type=single分支,直接调用了脚本里的extract_palette_with_scene函数。如果single分支写成独立模块,结构会更清晰。真实项目中,建议把掩码、聚类、文件读取逻辑拆成三个独立模块。这里为了减少文件数量,把主要逻辑放在一页脚本中,方便直接复制试验。

运行批量处理:

cd color-garden python scripts/run_pipeline.py

如果一切正常,会在output/palettes.json中看到每张照片对应的色卡和位置。检查输出时,不要只看是否有 JSON 文件,还要随机挑两三张图片,人工对比照片主色和 JSON 中占比最高的颜色是否一致。这一步非常重要:算法指标再好,最终仍要服务于人的视觉判断。

8. 用颜色地图把空间信息画出来

得到palettes.json后,最后一步是绘制“石家庄植物园颜色地图”。由于我们没有使用真实园区 GIS 图层,这里的做法是用metadata.csv中的相对坐标,在空白画布上绘制代表色圆点。

创建scripts/build_map.py

# scripts/build_map.py import json from pathlib import Path import matplotlib.pyplot as plt import pandas as pd from matplotlib.patches import Circle def draw_color_map(): project_root = Path(__file__).resolve().parent.parent metadata_path = project_root / "data" / "metadata.csv" result_path = project_root / "output" / "palettes.json" output_path = project_root / "output" / "plant_color_map.png" df = pd.read_csv(metadata_path) with open(result_path, "r", encoding="utf-8") as fp: results = json.load(fp) fig, ax = plt.subplots(figsize=(10, 8)) ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.set_aspect("equal") ax.set_title("Plant Garden Color Map") ax.set_xlabel("relative x") ax.set_ylabel("relative y") for row in df.itertuples(index=False): image_file = row.file_name if image_file not in results: continue x, y = row.x, row.y palette = results[image_file]["palette"] area = row.area # 每个地点最多取前 3 个代表色,并排画点 top_colors = palette[:3] n = len(top_colors) offsets = [(index - (n - 1) / 2) * 0.015 for index in range(n)] for color_item, offset in zip(top_colors, offsets): circle = Circle( (x + offset, y), radius=0.009, color=color_item["hex"], ) ax.add_patch(circle) ax.text(x, y + 0.035, area, ha="center", fontsize=8, color="dimgray") plt.tight_layout() plt.savefig(output_path, dpi=200) print(f"颜色地图已保存到 {output_path}") if __name__ == "__main__": draw_color_map()

运行后,打开output/plant_color_map.png,会看到不同坐标位置上分布着一组组颜色圆点。同一位置出现多个颜色圆点,说明这个位置的颜色构成不是单一的。这张图可以回答很多直观问题:入口处的色调是什么,水边是什么色调,哪个方向有更大范围的彩色花境。

由于这里只记录拍摄点位置,没有模拟真实路径,所以不构成图层意义上的精确地图。但在没有 GIS 底图的条件下,把它当作“园区颜色示意地图”是够用的。如果后续拿到真实园区平面图,可以手工将平面图作为 Matplotlib 的背景图片,再把颜色点映射到平面图的像素坐标,输出效果会更好。

9. 如何验证提取结果是否可信

颜色提取很容易出现“看起来在跑,结果却是错的”的情况。因为算法不会报错,但输出的颜色明显不符合实际。需要建立一套人工验证流程。

第一步,检查掩码。处理单张图片时,保存掩码和原图叠加效果。如果掩码保留了天空、道路、游客,说明阈值区间太宽;如果掩码把花卉主体删掉,说明颜色区间没有覆盖目标对象。

第二步,用 K 值做敏感性测试。将 K 分别设为 3、5、8,观察调色板的变化。如果 K 从 3 变到 5 时,新增颜色只是把原来的深绿拆成两个相近颜色,而不是出现新的花朵颜色,说明聚类结果被大面积绿色主导,需要调整掩码策略。

第三步,用肉眼抽查。把照片的顶部代表色和调色板放到一起做“匹配”,由人判断是否对应。不要只在单一显示器上判断,因为不同屏幕色差会影响判断。如果做严谨数据发布,应该使用色彩校准过的显示器,或者统一用 Lab 空间的色差值作为比较指标。

第四步,检查坐标异常。地图上颜色点的分布应该符合拍摄分布。如果所有点堆在同一个角落,说明 metadata 里的 x、y 坐标没有正确记录,要回头看数据表。

10. 常见问题与排查思路

问题现象可能原因排查方式解决方案
程序能运行,但输出颜色总体偏灰KMeans 把阴影、树干和叶子反光聚成灰色中心查看掩码是否只选中植物;检查原图光照条件掩码中加入饱和度下限;尝试转 Lab 后聚类
输出色号与照片显示颜色相差很大OpenCV 的 BGR 顺序被当成 RGB 使用打印 centers 的前三个数值,确认通道顺序使用 bgr_to_hex 转换通道顺序
花朵颜色没有出现在调色板中花色区域像素占比太低,被绿叶颜色掩盖统计掩码中非绿色像素数量;观察花朵是否过小单独对花色区域做聚类,或调低 K 值后用视觉筛选
GrabCut 运行速度很慢原始图片分辨率过高检查是否在缩放前运行先将最长边缩到 720 像素
中文路径图片读不出来OpenCV 对部分系统环境的中文路径支持有限检查cv2.imread返回是否 None使用cv2.imdecode(np.fromfile(..., np.uint8), cv2.IMREAD_COLOR)读取
不同照片同类植物颜色不一致天气、时间、白平衡差异导致查看拍摄时间和 EXIF 白平衡信息拍摄时加入标准色卡;按时间段分组分析
整体仍是大面积绿色,无法体现“颜色地图”观赏性色彩被大范围叶片压制查看拍摄对象是否足够靠近花卉分区统计非绿色色相,或选用以花为主的照片

如果出现第一类偏灰问题,优先去查看掩码可视化,而不是修改 KMeans 迭代次数。聚类算法本身没有“偏好灰色”的倾向,偏灰往往是因为输入像素里包含了太多非植物颜色。

11. 最佳实践与工程建议

这类“用颜色打开植物园”的项目,想做到可靠有说服力,需要在采集、记录、算法和输出四个环节都保持纪律。

拍摄时尽量选在上午 9 点到 11 点或下午 3 点到 5 点,顺光或侧光拍摄,避免正午强光造成叶片过曝。每拍完一组场景,先拍一张标准灰卡或者白纸,后面分析时可以用灰卡做白平衡校正。不要走几步就换一个曝光补偿,尽量维持一致的拍摄参数。把手机或相机的 EXIF 信息保留,它会记录拍摄时间、位置和白平衡,是后期排查的重要线索。

记录方面,metadata.csv 是项目的地基。字段不要只填路径,建议至少包含拍摄区域、相对坐标、场景类型、天气、备注。一个常见的错误是拍完照片后靠回忆补录位置,结果大量照片的位置信息错乱,导致颜色地图出现明显偏离。正确的做法是每次拍完一个分区,马上在表格里填入位置和场景类型。

算法方面,不要在单张图上寻找一组“万能 HSV 阈值”。把阈值从写死变量提升为配置项,让不同场景可以灵活切换。每次参数调整都记录原因和结果。如果发现某些阈值只适配某一张照片,要警惕过度拟合,应该扩大测试图片集。最终用户看到的不是某张照片的效果,而是系统在不同条件下的整体稳定性。

安全边界也要考虑。如果照片拍到了其他游客,在公开发布之前必须对人脸进行打码或直接裁掉,否则会涉及肖像权问题。植物园内部很多区域有围栏和管理要求,不要为了采集样本跨越围栏或进入禁止入内的花圃。石家庄植物园是一座正常开放的园区,正常情况下只要遵守园区规定、不破坏植物、不影响他人游园,拍摄分析样本没有问题。

输出方面,可以进一步做两件有价值的事:一是增加季节对比,同样坐标,春季、夏季、秋季各采一次样,就能得到“同一个园区的季相颜色变化”;二是做颜色距离分析,用 Lab 空间计算不同分区代表色的差异,找出哪些区域颜色接近、哪些区域反差大。前者有意思,后者有规划参考价值。

12. 结语与后续学习方向

说回这个项目的起点:我们不是真的要把石家庄植物园变成一份“色号大全”,而是想验证一个思路——用图像处理工具把一个感性的空间体验,压缩成一组可以比较、检索和可视化的颜色数据。单张照片是色卡,一批照片是颜色库,配上拍摄坐标之后,就变成颜色地图。这条链路不依赖特定园区,也不依赖特定植物种类,核心就是“图像读取—预处理—掩码—聚类—入库—可视化”这套通用工作流。

如果你已经跑通以上代码,下一步不需要急着换更多参数。可以试着扩大数据量,选取不同季节、不同天气的照片,统计某个区域的色相分布直方图;也可以把output/palettes.json接入一个小型 Web 页面。颜色地图不是只能用 Matplotlib 画,你还可考虑把每个点变成一个圆形色板,让用户点击后看到该区域完整色卡。

尤其建议你做一个“时间维度”的扩展:同一片区域,三月份、五月份、十月份各拍一组照片,不需要太多数量,每组十几张就够。用这套代码分别提取主色,再做色相直方图对比,就能直观看到植物园的颜色从嫩绿、翠绿到金黄的变化。这个视角一旦建立起来,你会发现自己身边所有空间都可能成为下一个颜色数据集。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询