
当我们选择用颜色打开石家庄植物园先把“颜色”这件事讲清楚。石家庄植物园的照片一旦拍多了翻相册找花就会变成一件苦差事。想找一张粉色系的花卉图想调出一片秋天的黄叶只能靠记忆来回翻。与其继续按日期、按相机型号归档不如把“颜色”当作主索引提取每张照片里最醒目的主色调按色相排成一条色带于是植物园的照片档案就变成了可以被检索的颜色序列。这轮我做的就是这样一个轻量项目用 Python 对植物园照片做颜色量化与主色归类再基于这些颜色数据做一个能按颜色筛选照片的可视化页面。它不依赖大模型不需要独立显卡CPU 就能跑完全流程。石家庄植物园只是测试数据来源你换成本地相册、另一个植物园或城市风光照片代码逻辑完全不用改。下面是完整实现过程包括颜色提取、批量索引、Streamlit 交互页面、API 化改造与问题排查。1. 像素色卡这个项目的核心能力速览先说清楚这个项目能做什么避免大家看一半才发现不是自己想要的。能力项说明项目类型植物园/花卉照片的颜色可视化与视觉检索小项目主要功能提取照片主色、按色系归档、按颜色浏览植物园照片技术路线OpenCV HSV 色相统计 KMeans 颜色聚类 Streamlit 展示硬件要求CPU 即可完成不依赖 GPU/CUDA运行方式Python 脚本批量预处理 Web 页面交互输入数据石家庄植物园照片或其他植物园/花卉照片文件夹中间产物CSV 格式的颜色索引表可视化能力按色系筛选照片、查看单张照片主色接口能力可改装为 FastAPI 服务供程序批量调用批量任务支持目录级批量处理适合场景个人照片管理、植物园科普展示、教学演示、摄影作品色系研究需要强调的是这里的“颜色索引”不是植物识别。它解决的是“我想找一张粉色的花/一组秋天的黄叶但不想从上千张图里手动翻”的问题。如果你需要识别具体植物品种那是另一套结合官方名录和花器官特征的系统不在本项目的范围内。2. 适用场景与使用边界这类颜色索引项目我认为最合适的场景有三个。第一个是个人素材库管理。很多摄影爱好者、自媒体编辑手里都有一堆植物园或花卉照片平时只能靠关键词盲找。给照片提取主色、按色相归档以后人物图、暗调图、绿色背景占大面积的图会被自动分开找图会直观很多。第二个是科普或展示场景。植物园的科普牌、园区公众号、研学课件都可以把“色卡 植物照片 季节”结合在一起比如春天做粉色系花卡、秋天做橙黄色系叶卡。这比纯文字讲解更符合植物园的视觉属性。第三个是教学演示。HSV 色彩空间、像素聚类、主色提取是计算机视觉入门里非常经典的案例拿真实植物园照片来讲比用一张纯色色块图有说服力。边界也很明显。第一颜色分类不能保证照片主体是花还是叶。如果背景有大面积草地绿色很容易把花朵的粉红色淹没。第二同一朵花在不同时间、不同天气、不同白平衡下提取出来的主色会有差异这和植物品种无关。第三色系归类是粗粒度经验值不能把“颜色”当作植物分类的可靠依据。如果你要发布照片或把结果商用记得确认素材是否有授权以及园区是否有拍摄用途限制。如果照片里有清晰人脸还要考虑肖像权问题。3. 本地部署前的环境准备这个项目对硬件非常友好普通笔记本就可以。在准备环境前先想清楚你要跑哪一部分只做颜色提取和 CSV 输出Python 环境 opencv-python scikit-learn pandas 就够了。要做交互可视化再加一个 streamlit。要做接口服务再加 fastapi 和 uvicorn。建议用虚拟环境做隔离不要直接往系统 Python 里装一堆依赖否则后面项目多了容易冲突。python -m venv venv source venv/bin/activate # Windows 上使用 venv\Scripts\activate装依赖pip install opencv-python numpy pandas scikit-learn matplotlib streamlit fastapi uvicorn装好后可以确认版本python -c import cv2, sklearn, pandas; print(cv2.__version__)数据目录可以这样规划shijiazhuang-botanic-color/ ├── data/ │ ├── raw/ # 原始植物园照片 │ └── clipped/ # 需要裁切测试的小图 ├── output/ │ ├── color_index.csv # 颜色索引表 │ └── main_colors/ # 每张图的主色色卡 ├── app.py # Streamlit 可视化页面 ├── color_utils.py # 主色提取与颜色分类函数 └── batch_index.py # 批量生成颜色索引原始图片不要直接堆在项目根目录后续批量测试时容易把无关图片一起读进来。4. 从照片到主色颜色提取是怎么跑的颜色提取是整个项目的地基。这里采用两个核心步骤第一步把照片从 RGB 转换到 HSV 色彩空间。HSV 里 H 代表色相也就是人眼感知的“这是什么颜色”S 饱和度代表颜色的鲜艳程度V 明度代表亮暗。相比 RGBHSV 更适合按色系做分析因为色相通道天然就是一条连续的色调环。第二步用 KMeans 对像素做聚类。一张照片可能有几十万像素直接统计每个颜色会很碎。KMeans 可以把所有像素聚成 3 到 5 个颜色中心每一个中心代表一种视觉上比较有代表性的主色。先写一个color_utils.py把全流程拆成函数。import cv2 import numpy as np from sklearn.cluster import KMeans def read_image(path): 读取图片。用 imdecode 而不是 imread目的是兼容中文路径。 data np.fromfile(path, dtypenp.uint8) image cv2.imdecode(data, cv2.IMREAD_COLOR) if image is None: raise ValueError(f无法读取图片{path}) return cv2.cvtColor(image, cv2.COLOR_BGR2RGB) def extract_main_colors(image_rgb, n_colors4, sample_size20000, random_seed0): 从 RGB 图像中提取 n_colors 个主色。 为了避免几十万像素全部参与聚类先进行一次随机抽样。 pixels image_rgb.reshape(-1, 3).astype(np.float32) if len(pixels) sample_size: rng np.random.default_rng(random_seed) idx rng.choice(len(pixels), sample_size, replaceFalse) pixels pixels[idx] kmeans KMeans(n_clustersn_colors, n_init5, random_staterandom_seed) kmeans.fit(pixels) colors np.round(kmeans.cluster_centers_).astype(int) counts np.bincount(kmeans.labels_) order np.argsort(-counts) total counts.sum() result [] for i in order: rgb colors[i] hex_color #{:02x}{:02x}{:02x}.format(rgb[0], rgb[1], rgb[2]) result.append({ rgb: ,.join([str(c) for c in rgb]), hex: hex_color, share: float(counts[i] / total) }) return result这里有几个细节值得说明。KMeans 聚类前先对像素随机抽样是为了避免高分辨率照片运行太慢。20000 个像素已经能代表一副图的整体颜色分布并且聚类结果足够稳定。最后的占比是基于抽样样本的近似结果不是全图逐像素占比但对于色卡排序来说完全够用。KMeans 每一次运行的随机性会导致颜色顺序不稳定。代码里固定了random_state0这样多次运行同一张图输出主色顺序基本一致。5. 让颜色变成色系RGB 主色转成色相标签仅仅拿到主色的十六进制值还不够。我们要做的是“红色系、橙黄色系、绿色系、蓝青色系、紫色系”需要把主色映射到宽泛的色相区间。写一个颜色家族判断函数输入主色的 RGB 十六进制输出视觉上的粗粒度色系。import colorsys def color_family(hex_color, low_sat_limit0.18, low_val_limit0.15): 给出一个粗略的色系归类。 仅用于植物园照片的展示和检索不承担精确色彩学定义。 hex_color hex_color.lstrip(#) r, g, b (int(hex_color[i:i2], 16) for i in (0, 2, 4)) h, s, v colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) if v low_val_limit: return 暗部/黑色 if s low_sat_limit: return 低饱和/灰白 if h 0.05 or h 0.95: return 红色系 if h 0.12: return 橙色系 if h 0.20: return 黄色系 if h 0.45: return 绿色系 if h 0.70: return 蓝青色系 return 紫色系这个函数不是色彩学标准只是一个方便检索的粗标签。比如你拍了一张以大面积为绿色叶片、中间一朵粉色月季的照片KMeans 提取出来占比最大的很可能是绿色所以主色排序大概率会把“绿色系”放在前面。在用颜色检索照片时这点要心里有数。如果想把某个色系调得更严格把h的区间边界改一改就行不用动其他代码。6. 批量处理把整个植物园照片文件夹变成颜色索引有了单张图的主色提取函数下一步就是批量遍历文件夹。这里我把石家庄植物园的照片统一放在data/raw目录下脚本会逐个读取把每张图最重要的几个主色、占比和色系写入 CSV。from pathlib import Path import pandas as pd from color_utils import read_image, extract_main_colors, color_family def build_color_index(input_dir, output_csv, n_colors3, top_n3): 遍历目录下的 jpg/png输出颜色索引 CSV。 默认只保留每张图占比最高的 top_n 个主色。 image_paths list(Path(input_dir).rglob(*.jpg)) image_paths list(Path(input_dir).rglob(*.png)) if not image_paths: raise ValueError(f目录下没有 jpg/png 图片{input_dir}) rows [] for path in image_paths: try: image_rgb read_image(str(path)) colors extract_main_colors(image_rgb, n_colorsn_colors) except Exception as exc: print(f[跳过] {path.name}: {exc}) continue for color in colors[:top_n]: rows.append({ image_name: path.name, image_path: str(path), hex: color[hex], share: color[share], family: color_family(color[hex]), }) df pd.DataFrame(rows, columns[image_name, image_path, hex, share, family]) df.to_csv(output_csv, indexFalse, encodingutf-8-sig) print(f已输出 {len(rows)} 行颜色索引到 {output_csv}) return df if __name__ __main__: build_color_index(data/raw, output/color_index.csv)注意几个工程细节。第一输出 CSV 使用utf-8-sig编码这样直接用 Excel 打开时中文不会乱码。第二脚本对单张图做了异常捕获。植物园照片里偶尔会有 RAW 转 JPG 时损坏的文件或者是尺寸为 0 的空文件不应该因为一张坏图中断整个批处理。第三因为 CSV 里同时存了image_path后面做可视化页面时可以直接按行读取路径展示图片不用重新按文件名拼接目录。跑完以后用 pandas 或者文本编辑器看 CSV每一行结构大致是image_name image_path hex share family 紫藤_001.jpg data/raw/紫藤_001.jpg #7b6ca8 0.42 紫色系 荷花_023.jpg data/raw/荷花_023.jpg #bd2f3f 0.38 红色系 植物园蓝门.jpg data/raw/植物园蓝门.jpg #1e6f9c 0.27 蓝青色系到这一步图片库已经被转换成一张“颜色—色系—文件”的映射表。7. 用 Streamlit 做颜色检索页面颜色索引 CSV 生成后就可以做交互浏览页面了。Streamlit 是最快的方式不写前端代码直接写 Python 逻辑就能生成可筛选的 Web 页面。app.py的核心逻辑很简单读取 CSV让用户选择色系然后过滤出对应图片并展示。import pandas as pd import streamlit as st from PIL import Image st.cache_data def load_index(csv_path): return pd.read_csv(csv_path) def main(): st.set_page_config(page_title石家庄植物园 · 颜色索引, layoutwide) st.title(当我选择用颜色打开石家庄植物园) st.markdown(选择一个色系查看对应照片的主色信息。) df load_index(output/color_index.csv) families [全部] sorted(df[family].unique()) selected st.selectbox(按色系筛选, families) if selected ! 全部: filtered df[df[family] selected] else: filtered df st.write(f当前显示 {len(filtered)} 条主色记录。) cols st.columns(4) for idx, row in filtered.iterrows(): col cols[idx % 4] with col: st.image(row[image_path], captionf{row[image_name]} / {row[hex]}, use_container_widthTrue) st.text(f色系{row[family]} 占比{row[share]:.2f}) if __name__ __main__: main()启动页面streamlit run app.pyStreamlit 默认开在 8501 端口启动成功后在浏览器访问http://localhost:8501就能看到页面。实际操作中建议把每个色系下的图片数量当做第一步验证标准。比如“绿色系”照片数量如果特别多这是符合预期的因为植物园里树木和草坪占比高。但如果你拍的主要是花卉特写绿色依然占主力就需要考虑在拍摄时把花朵主体裁切得更大或者后续引入注意力区域裁剪。让页面可用性再高一点还可以把hex标准成一块真实颜色的小卡片展示出来。Streamlit 的原生组件没有专门的颜色方块最简单做法是转成 HTML 叠加亮度控件或者直接输出一个纯色PIL.Image对象def render_color_block(hex_color, width80, height40): img Image.new(RGB, (width, height), hex_color) return img然后在st.image里把原图和一个色块放在同一行就能更直观地看出“这张照片的主色到底是什么”。8. 接口化改造给颜色索引开一个 API 入口Streamlit 页面适合人机交互但如果你想把图片分析能力接到自己的工具链里还是需要一个接口。这里用 FastAPI 把主色提取能力暴露成 HTTP 接口。写一个独立文件api.pyfrom fastapi import FastAPI, UploadFile, File, Form import tempfile from color_utils import read_image, extract_main_colors, color_family app FastAPI(titlePlant Color Index API) app.post(/extract) async def extract_main_color(file: UploadFile File(...), n_colors: int Form(3)): # 先暂存上传文件再读取为图片 suffix . file.filename.rsplit(., 1)[-1].lower() with tempfile.NamedTemporaryFile(deleteFalse, suffixsuffix) as tmp: tmp.write(await file.read()) tmp_path tmp.name image_rgb read_image(tmp_path) colors extract_main_colors(image_rgb, n_colorsn_colors) result [] for color in colors: result.append({ hex: color[hex], share: color[share], family: color_family(color[hex]), }) return {filename: file.filename, colors: result}启动接口服务uvicorn api:app --host 0.0.0.0 --port 8000用一个简单的 curl 命令测试curl -X POST http://127.0.0.1:8000/extract \ -H Content-Type: multipart/form-data \ -F filedata/raw/test.jpg \ -F n_colors3返回 JSON 的结构大概是{ filename: test.jpg, colors: [ { hex: #7b6ca8, share: 0.42, family: 紫色系 }, { hex: #4a5d23, share: 0.31, family: 绿色系 }, { hex: #e8dcc2, share: 0.20, family: 低饱和/灰白 } ] }需要说明的是这个 API 是配合自建项目使用的通用示例字段和路径都可以按自己的项目结构调整。接口能跑通意味着你可以把它接到自动图片分类脚本、CI 流程或者内容管理系统里。如果只想批量处理不走 HTTP也可以用前面第 6 节的batch_index.py。两种方式不冲突小批量测试用 API大批量整理用目录脚本。9. 资源占用与性能观察这个项目不吃显存不像部署大语言模型或图像生成模型那样需要关心 CUDA 和显存占用整体性能压力都在 CPU、内存和磁盘读写上。运行批量处理时可以打开系统监控工具观察这几个指标观察对象说明CPU 占用率KMeans 聚类会占满单核或部分多核内存占用主要由读取图片后像素数组的大小决定磁盘队列批量读取大量高分辨率照片时明显是否出现物理内存不足大尺寸照片 大批量循环时要注意Python 的cv2.imread会把整张图片解码进内存所以照片分辨率越高单张占用越大。20 张 4000x3000 的图片一起循环内存压力明显高于 50 张 1000x750 的图。想降低资源占用可以从三方面入手。第一批量脚本里先做图片缩放。比如把最长边控制在 1024 像素以内视觉主色几乎不会有感知级变化但内存占用会下降很多。第二抽样像素数量不要设得太大。extract_main_colors默认抽样 20000 个像素已经足够稳定。把sample_size调到 50000只会更慢不会让主色结果明显变准。第三不要把输入图片都放在同一个超大目录下一把梭。按园区、季节、相册拆分目录分批生成索引方便出问题时排查。从启动速度来看单个 Python 脚本启动只花秒级时间Streamlit 和 FastAPI 首次启动会略慢因为需要导入 OpenCV、pandas 等重型依赖但服务起来后响应并不慢。如果导入很卡优先检查是不是装了多个版本的 opencv 或者把opencv-python和opencv-contrib-python同时装进了同一个环境。10. 常见问题与排查方法整个流程不复杂但植物园实拍照片往往比测试图脏得多颜色分布的干扰也很多。问题现象可能原因排查方式解决方案启动后页面打不开Streamlit 端口被占用查看启动日志和端口占用换端口如streamlit run app.py --server.port 8502图片读不出来文件路径带中文或非 ASCII 字符打印报错信息改用cv2.imdecode(np.fromfile(...))方式读取图片读不出来文件格式损坏单独测试这张图是否能被打开备份后重新导出或删除坏图提取的主色偏灰白暗部/白色背景占比大看聚类中心占比观察原图背景裁剪主体区域或过滤极低饱和度颜色主色顺序和预期不一样KMeans 聚类中心和人工感受不同对比多次运行结果固定 random_state增加聚类中心数量CSV 中文乱码编码设置错误用文本编辑器检查 CSV 编码输出encodingutf-8-sig花是红的但归到了绿色大面积绿叶淹没花朵像素检查主色的 top 顺序对花朵做局部裁剪后重新提取接口返回 500上传文件格式异常看后端日志增加异常捕获并返回更明确的错误信息批量跑得很慢原图分辨率太高观察内存和 CPU先缩放最长边到 1024 像素结果分类不稳定HSV 区间边界设置过窄测试同一张不同天气照片放宽色相阈值或改用统计聚类标签这里最值得提前注意的是中文路径问题。Windows 上cv2.imread读中文路径图片经常直接返回None。解决办法就是前面已经用到的cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)。这个看起来很小的问题实际遇到时很容易卡壳建议直接写进读取工具函数里。另一个常见问题是“颜色到底应该怎么归类”。纯色相区间划分依赖经验不同拍摄时间的色温差异也会带来偏差。如果对一个色系的确定性很高再用 HSV 区间判断如果只是做浏览器呈现用一个聚类后返回结果的方法会更稳同时还能避免多人手工标注的主观差异。11. 最佳实践与使用建议跑通基本流程之后建议把下面这几条固化成自己的工作习惯。先建目录在动手。原始图像放一个目录中间产物放另一个目录结果输出单独放一个目录。即使你只在本地跑几个小脚本也要避免图片和代码散落在一起。第一次运行不要直接处理全部图片。先挑选 10 到 20 张有代表性的照片覆盖不同季节、不同光线、不同花色观察颜色提取是否符合预期。等分类效果稳定了再大批量跑。给“颜色索引”加时间戳版本。石家庄植物园不同季节拍出来的颜色分布完全不一样。你在夏天跑出的结果和秋天跑出的结果合并到一张 CSV 会造成季节性混乱。建议 CSV 文件命名里带日期或季度关键词。在批量脚本里加上日志输出。万一某张图处理失败了至少能快速定位是哪个文件、哪个环节出错。不要只打印一个 “error” 就继续把图片名和异常原因尽量带出来。正则化输出格式。如果你要长期积累建议在 CSV 里同时保留hex、rgb、hue_degree、family、image_path。以后要做散点图、做季节颜色对比时这些字段都是现成的。接口服务如果部署到局域网或服务器要限制访问范围。本地调试用127.0.0.1需要其他设备访问时再改成0.0.0.0。不要把接口无脑暴露到公网除非你已经加了身份验证。凡是涉及人脸或肖像的植物园照片发布到公开网络前要再次审查。这个颜色索引项目本身不会生成新内容但作为内容管理工具它可能会让原本零散的照片更容易被检索和分发发布授权问题就要提前确认。12. 总结与下一步这个项目真正值得做的点是它把一套相对枯燥的颜色统计流程变成了一个“用颜色重新打开植物园照片库”的实用工具。核心代码里没有复杂网络模型全流程都可以在本地 CPU 环境跑通而且无论你是想管理植物园照片还是做任意题材的图片素材整理这套逻辑都能复用。先验证的第一件事是拿三张颜色特征特别明显的图比如一张大面积绿叶图、一张粉红色花朵特写、一张深色水面倒影跑一次extract_main_colors看主色和占比是否符合感知。这个测试通过后再批量生成全园索引就不容易翻车。最容易踩的坑是中文路径读图和花朵被绿叶淹没这两个问题。建议把它们当作必测项处理而不是等批量跑到一半才暴露。下一步可以扩展的方向很多。你对颜色有了统一索引之后还能计算整个石家庄植物园初夏和深秋的平均色相差异也可以每天拍一张代表性植物图记录主色变化曲线做出一年四季的颜色演变图。再往前一步可以给同一园区不同区域分别建立颜色索引就能做出“园区—季节—颜色”的多级检索地图。这套流程本身不难难的是把颜色数据持续积累成体系。先用一个周末拍一批照片、跑一遍脚本把第一版颜色地图做出来之后再慢慢调整色系划分和可视化方式。