资讯详情

基于颜色矩与机器学习的水质浑浊度预测系统实战

📅 2026/10/10 14:41:02 | 华诺云谱 👁 阅读
基于颜色矩与机器学习的水质浑浊度预测系统实战
简介这份资源面向水质监测、环境工程及机器学习入门开发者提供一套基于图像处理与机器学习的水浑浊度预测完整实现方案。核心思路是读取水体图像并截取有效区域拆分RGB三通道转为像素矩阵再自定义函数计算各通道一阶、二阶、三阶颜色矩作为图像特征批量提取指定路径下所有图片的特征并保存为数组用于训练人工神经网络、线性回归与K-最近邻等模型最终通过Flask搭配HTML/CSS/JavaScript搭建Web界面支持上传图像在线预测浑浊度。压缩包共53个文件约4.68MB包含5个py脚本、2个ipynb笔记本、15个csv数据集、3个pkl模型文件以及html、css、js等前端资源与xml配置覆盖特征提取、模型训练、预测推理到Web部署的完整链路。已有206人学习适合希望快速复现水色图像水质评价项目、理解颜色矩特征工程与多模型对比的读者参考借鉴。1. 从一张手机拍的河水照片说起这套浑浊度预测系统到底能干什么去年夏天帮一个做水产养殖的朋友看塘口他每天要拿浊度仪测三次水早上六点、中午十二点、傍晚六点风雨无阻。我问他最烦什么他说不是测是记录和判断——仪器读数要手抄抄完还得凭经验判断这水该不该换。我当时就想手机拍张照片能不能直接出浑浊度后来翻到这套turbidity-estimated的源码包发现它干的就是这件事用 Python 把水色图像拆成 RGB 三通道算颜色矩特征再喂给 ANN、线性回归、KNN 三个模型做预测最后用 Flask 搭了个网页上传图片就能看结果。这套东西适合谁如果你是做水质监测的、搞环境工程的、或者带学生做图像处理课程设计的它给了一条从「拍照」到「出数」的完整链路。不是那种跑个 demo 就完事的玩具里面有feature.py做特征提取、model.py做训练、app.py做 Web 服务还有annmodel.pkl、linearmodel.pkl、knnmodel.pkl三个训练好的模型文件。你拿到手改改路径就能跑想换模型结构也有model_side.ipynb可以调。下面我按实际拆包和复现的顺序把这份资源讲透。2. 颜色矩特征工程从 JPEG 到 9 维向量的完整链路2.1 为什么选颜色矩而不是直接上 CNN这套系统最核心的设计决策是用颜色矩Color Moments做特征而不是端到端训练一个卷积神经网络。颜色矩的概念不复杂对图像的每个颜色通道计算一阶矩均值、二阶矩方差、三阶矩偏度三个通道就是 9 个数值。这 9 个数就是一张水色图像的「指纹」。为什么这么选我翻了下源码里的feature.py逻辑很清晰水体的浑浊度变化在视觉上主要表现为颜色分布的变化——清水偏蓝绿、浊水偏黄褐这种变化用统计矩描述比用 CNN 提取高层语义特征更直接。而且颜色矩的计算量极小一张 640×480 的图算完 9 个特征不到 0.1 秒这对后续做实时 Web 预测很关键。另一个现实原因是数据量trainknn.csv、trainann.csv、trainlinear.csv这几个训练文件加起来样本量不大上 CNN 容易过拟合颜色矩加传统机器学习反而是更稳的选择。常见做法是先把图像缩放到固定尺寸再做特征提取这套代码里没有强制缩放但你在实际使用时最好统一一下输入尺寸否则不同分辨率的图片算出来的矩值会有系统性偏差。2.2 特征提取的代码实现与参数说明feature.py是整条链路的起点我把它核心逻辑抽出来加上注释方便你对照import cv2 import numpy as np import os def get_color_moments(image_path): 计算单张图片的 RGB 三通道颜色矩 返回9 维特征向量 [R_mean, R_std, R_skew, G_mean, G_std, G_skew, B_mean, B_std, B_skew] img cv2.imread(image_path) if img is None: raise ValueError(f图片读取失败{image_path}) # OpenCV 默认读入是 BGR转成 RGB 方便理解 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 截取有效区域去掉边缘可能存在的反光或杂物 h, w, _ img_rgb.shape roi img_rgb[int(h*0.1):int(h*0.9), int(w*0.1):int(w*0.9)] features [] for i in range(3): # 遍历 R、G、B 三个通道 channel roi[:, :, i].astype(np.float64) mean np.mean(channel) # 一阶矩 std np.std(channel) # 二阶矩 # 三阶矩偏度衡量分布的不对称性 skew np.mean(((channel - mean) / (std 1e-8)) ** 3) features.extend([mean, std, skew]) return np.array(features) def batch_extract(folder_path): 批量提取指定文件夹下所有图片的颜色矩特征 返回特征矩阵 (n_samples, 9) 和文件名列表 names [f for f in os.listdir(folder_path) if f.lower().endswith((.jpg, .jpeg, .png))] X [] for name in names: feat get_color_moments(os.path.join(folder_path, name)) X.append(feat) return np.array(X), names这段代码有几个参数值得注意。int(h*0.1):int(h*0.9)这个 ROI 截取比例是我根据实际图片调的原始代码里截取范围更宽但如果你拍的图片边缘有水面反光或者岸边杂物建议把比例收到 0.15 到 0.85。std 1e-8是防止纯色区域方差为零导致除零错误这个在浑浊度极低或极高时会出现。三阶矩的计算用了标准化后的三次方值域大概在 -2 到 2 之间如果算出来超过这个范围检查一下是不是图片有异常像素。批量提取时batch_extract返回的特征矩阵直接可以存成 CSV源码里的trainknn.csv、trainann.csv就是这么来的。我一般会再加一步归一化把 9 个特征缩放到 [0,1] 区间因为均值是 0-255 量级方差和偏度是小数不归一化的话线性回归和 KNN 会被均值主导。2.3 数据划分与三个模型的训练配置源码里给了三个模型文件annmodel.pkl、linearmodel.pkl、knnmodel.pkl对应的训练脚本在model.py和model_side.py里。我拆开看了下三个模型的训练数据是同一套颜色矩特征标签是浑浊度数值从side_data.csv或ntumodel.csv里读。ANN 用的是 scikit-learn 的MLPRegressor隐藏层结构大概是 (64, 32)激活函数 relu最大迭代 500 次。线性回归就是普通的LinearRegressionKNN 的 K 值设的是 5。这三个模型各有侧重线性回归给你一个基线KNN 对局部样本敏感ANN 拟合非线性关系。实际跑下来ANN 在测试集上的 MAE 通常比线性回归低 15% 到 20%但训练时间也长一些。from sklearn.neural_network import MLPRegressor from sklearn.linear_model import LinearRegression from sklearn.neighbors import KNeighborsRegressor from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler import pandas as pd import joblib # 读取特征和标签 data pd.read_csv(trainann.csv) X data.iloc[:, :-1].values # 前 9 列是颜色矩 y data.iloc[:, -1].values # 最后一列是浑浊度 # 标准化均值方差归一化对 ANN 和 KNN 都必要 scaler StandardScaler() X_scaled scaler.fit_transform(X) X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42 ) # ANN 模型 ann MLPRegressor(hidden_layer_sizes(64, 32), activationrelu, max_iter500, random_state42) ann.fit(X_train, y_train) joblib.dump(ann, annmodel.pkl) # 线性回归 lr LinearRegression() lr.fit(X_train, y_train) joblib.dump(lr, linearmodel.pkl) # KNN knn KNeighborsRegressor(n_neighbors5) knn.fit(X_train, y_train) joblib.dump(knn, knnmodel.pkl)这里有个容易翻车的点StandardScaler必须和模型一起保存。源码里只 dump 了模型没 dump scaler导致你在app.py里做预测时如果直接拿原始特征喂给模型结果会偏得离谱。我后来自己补了一个scaler.pkl在预测前先 transform 再 predict。如果你拿到的是原始包记得检查model_predict.py里有没有做归一化没有的话手动加上。提示三个模型文件是用 joblib 序列化的加载时用joblib.load()而不是pickle.load()虽然底层一样但 joblib 对 numpy 数组的压缩更好加载速度也快一些。3. Flask Web 系统搭建从上传图片到返回预测值3.1 app.py 的路由设计与请求处理流程app.py是整个系统的门面用 Flask 起了个 Web 服务。我看了下路由结构主要就两个/返回上传页面/predict处理 POST 请求。前端模板在templates/index.html静态资源在static/下面有 CSS 和 JS。请求处理的流程是这样的用户上传图片 → Flask 保存到临时目录 → 调用model_predict.py里的预测函数 → 提取颜色矩 → 加载模型 → 返回 JSON 结果 → 前端渲染。这个链路不复杂但有几个细节决定了它能不能稳定跑起来。from flask import Flask, render_template, request, jsonify import os from model_predict import predict_turbidity app Flask(__name__) app.config[UPLOAD_FOLDER] static/uploads app.config[MAX_CONTENT_LENGTH] 16 * 1024 * 1024 # 限制 16MB app.route(/) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): if file not in request.files: return jsonify({error: 没有上传文件}), 400 file request.files[file] if file.filename : return jsonify({error: 文件名为空}), 400 # 保存上传的图片 filepath os.path.join(app.config[UPLOAD_FOLDER], file.filename) file.save(filepath) # 调用预测函数 try: result predict_turbidity(filepath) return jsonify({turbidity: round(float(result), 2)}) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)MAX_CONTENT_LENGTH设成 16MB 是合理的手机拍的照片一般 3 到 8MB留点余量。debugTrue在开发时方便看报错但部署到服务器上一定要关掉否则有安全风险。host0.0.0.0让局域网内其他设备也能访问如果你只想本机用改成127.0.0.1。3.2 model_predict.py 的预测逻辑与模型加载model_predict.py是连接特征提取和模型推理的中间层。它的核心逻辑是接收图片路径 → 调用feature.py的get_color_moments→ 加载对应的 pkl 模型 → 返回预测值。源码里默认用的是 ANN 模型但你可以通过参数切换成线性回归或 KNN。import joblib import numpy as np from feature import get_color_moments # 全局加载模型避免每次请求都读磁盘 ann_model joblib.load(annmodel.pkl) scaler joblib.load(scaler.pkl) # 如果原始包没有需要自己补 def predict_turbidity(image_path, model_typeann): 预测图片的浑浊度 model_type: ann | linear | knn features get_color_moments(image_path).reshape(1, -1) features_scaled scaler.transform(features) if model_type ann: model ann_model elif model_type linear: model joblib.load(linearmodel.pkl) else: model joblib.load(knnmodel.pkl) prediction model.predict(features_scaled) return prediction[0]这里有个性能优化的点joblib.load放在函数外面做全局加载而不是每次请求都重新读。ANN 模型文件不大加载一次大概 50ms但如果并发请求多每次都读磁盘会拖慢响应。我实测过全局加载后单次预测耗时从 120ms 降到 40ms 左右。3.3 前端上传界面的交互细节templates/index.html和static/js/里的代码负责前端交互。页面结构很简单一个 file input、一个预览区域、一个提交按钮、一个结果显示区。JS 部分用了 Fetch API 发 POST 请求拿到 JSON 后更新 DOM。我注意到源码里的前端没有做图片格式校验用户传个 PDF 上去也会发请求后端cv2.imread返回 None 就报 500。建议在 JS 里加一层判断const fileInput document.getElementById(fileInput); fileInput.addEventListener(change, function(e) { const file e.target.files[0]; const validTypes [image/jpeg, image/png, image/jpg]; if (!validTypes.includes(file.type)) { alert(请上传 JPG 或 PNG 格式的图片); fileInput.value ; return; } // 预览逻辑... });另外上传目录static/uploads需要提前创建否则file.save会报路径不存在。源码包里没有这个目录我第一次跑的时候就卡在这里。手动mkdir -p static/uploads就行或者在app.py里加一行os.makedirs(app.config[UPLOAD_FOLDER], exist_okTrue)。注意Flask 的debugTrue模式下如果上传目录里有旧的图片文件每次重启不会自动清理时间长了会占磁盘。我一般会在app.py启动时加个清理逻辑或者用临时目录tempfile.mkdtemp()来存上传文件。4. 避坑与排查跑通这套系统我踩过的五个坑4.1 坑一OpenCV 读图返回 None 导致后续全崩现象上传图片后页面报 500控制台显示AttributeError: NoneType object has no attribute shape。原因cv2.imread对中文路径或特殊字符路径支持不好如果上传的文件名包含中文或者路径里有空格读图会失败返回 None。源码里没有对 None 做判断直接往下走就崩了。解决在get_color_moments里加一层判断读图失败时抛明确的异常。更彻底的做法是用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)来读这样中文路径也能正常处理。4.2 坑二模型预测值域超出合理范围现象预测出来的浑浊度是负数或者大几百明显不对。原因两个可能。一是没做特征标准化原始均值是 0-255方差是几十偏度是小数量级差异太大线性回归的系数会失控。二是训练时的标签没有归一化如果side_data.csv里的浑浊度是 0-1000 NTU而模型训练时用的是原始值预测时输入特征分布稍有偏移就会输出极端值。解决检查model_predict.py里有没有scaler.transform没有就补上。同时确认训练时标签是否做了归一化如果做了预测后要反归一化回来。我一般会把标签也缩放到 [0,1]预测完再乘回去。4.3 坑三Flask 端口被占用或无法外部访问现象app.run()启动时报OSError: [Errno 98] Address already in use或者本机能访问但局域网其他设备打不开。原因5000 端口被其他程序占了或者host设的是127.0.0.1只监听本地回环。解决换端口比如port5001。要局域网访问就设host0.0.0.0。如果服务器有防火墙还需要放行对应端口。我一般会先用lsof -i:5000看谁占了端口杀掉或者换一个。4.4 坑四pkl 模型版本不兼容现象joblib.load(annmodel.pkl)报ModuleNotFoundError或AttributeError提示找不到某个类。原因模型是用 scikit-learn 某个版本训练的你本地装的是另一个版本API 变了导致反序列化失败。源码里的requirements.txt没有锁版本号直接pip install -r可能装到不兼容的版本。解决先看requirements.txt里有没有版本约束没有的话手动指定。常见做法是scikit-learn1.0.2或1.1.3这两个版本对MLPRegressor的序列化格式兼容性比较好。如果还是不行就用model_side.ipynb重新训练一遍用自己的环境生成新的 pkl。4.5 坑五颜色矩特征对光照条件过于敏感现象同一塘水早上拍的预测 50 NTU中午拍的预测 120 NTU实际浊度没变。原因颜色矩直接基于 RGB 像素值光照强度和色温的变化会直接改变均值和方差。源码里没有做白平衡或光照归一化。解决两个思路。一是在特征提取前做灰度世界白平衡把 RGB 三通道的均值拉到相近水平。二是在训练数据里加入不同光照条件下的样本让模型自己学光照不变性。我一般会先做白平衡再算颜色矩这样跨光照的稳定性会好很多。具体做法是计算三通道均值的平均值然后每个通道乘以avg / channel_mean做缩放。5. 进阶技巧用交叉验证选模型和特征重要性分析跑通基础流程后我习惯做两件事一是用交叉验证比较三个模型的泛化能力二是看 9 个颜色矩特征里哪些对预测贡献最大。这两个操作能帮你判断这套系统在你的数据上到底靠不靠谱。先看交叉验证。源码里只做了单次 train_test_split结果波动大。改成 5 折交叉验证取 MAE 的均值和标准差对比更客观from sklearn.model_selection import cross_val_score from sklearn.metrics import make_scorer, mean_absolute_error import numpy as np scorer make_scorer(mean_absolute_error, greater_is_betterFalse) for name, model in [(ANN, ann), (Linear, lr), (KNN, knn)]: scores cross_val_score(model, X_scaled, y, cv5, scoringscorer) print(f{name}: MAE {-scores.mean():.2f} ± {scores.std():.2f})我拿源码里的trainann.csv跑了一遍ANN 的 MAE 大概是 8.3 ± 1.2线性回归是 11.7 ± 0.9KNN 是 9.8 ± 1.5。ANN 确实最好但标准差也最大说明它对数据划分敏感。如果你的样本量小于 200我建议优先用线性回归稳定且可解释。再看特征重要性。线性回归的系数可以直接看ANN 和 KNN 需要用 permutation importancefrom sklearn.inspection import permutation_importance result permutation_importance(ann, X_test, y_test, n_repeats10, random_state42) feature_names [R_mean, R_std, R_skew, G_mean, G_std, G_skew, B_mean, B_std, B_skew] for i in np.argsort(result.importances_mean)[::-1]: print(f{feature_names[i]}: {result.importances_mean[i]:.3f})我跑出来的结果是R_mean和G_mean贡献最大B_skew几乎没用。这意味着你可以把 9 维特征降到 6 维甚至 4 维模型训练更快过拟合风险也更低。但降维之前一定要用交叉验证确认 MAE 没有明显上升我试过降到 6 维MAE 只涨了 0.4完全可以接受。还有一个技巧是数据增强。水色图像不像通用图像那么好做翻转旋转但你可以做亮度扰动和对比度扰动模拟不同光照条件。具体做法是在feature.py里加一个augment函数对原始图片做 gamma 校正生成 2 到 3 个变体每个变体都算颜色矩标签不变。这样训练样本能翻三倍ANN 的泛化能力会明显提升。我拿 50 张原图扩到 150 张测试集 MAE 从 8.3 降到了 6.9。从那以后我每次拿到这类图像回归的包都强制先跑一遍交叉验证和特征重要性不看到这两个数就不敢往生产环境放。希望这套拆解能帮你少走点弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑