☰
昆虫识别与数目统计Java毕设工程:从特征提取到连通域计数
2026/10/4 21:52:40 网站建设 项目流程

简介:面向大四毕业设计、课程设计及作业场景,这是一套昆虫识别与数目统计的完整项目包,适用于计算机视觉、人工智能等相关方向的本科生参考或二次开发。压缩包共164个文件,整体大小14.59MB,核心内容包含97张昆虫图片构成的图像样本集、23个Python源码文件、13个npy数据文件、10个xml标注文件、3个已训练模型、3个csv统计表以及一个ui交互界面。这些材料覆盖了从图像采集、数据标注、模型训练到数量统计的完整技术链路,同时附带说明文档、许可证文件等,便于理解工程结构并快速运行验证。读者可以从零复现昆虫识别与计数流程,复用模型和脚本完成自己的实验或毕设任务。目前已经有一百四十人学习下载,整体上是课设、大作业、毕设场景中较为实用的参考资源。

1. 昆虫识别与数目统计毕设:这份压缩包能帮你省多少事

“昆虫识别”和“数目统计”是图像处理方向课设、大作业和毕业设计里高频出现的一道题:给一批昆虫图片,训练出能分辨种类并统计数量的程序。这个压缩包里装的是 data.csv、datatest.csv、ques.csv 三个数据集,外加一组 fly*.jpg 测试图和 IDEA 工程文件,一看就是能直接跑的完整骨架,不是网上那种论文配残缺代码的缝合怪。它适合两类人:一类是大四学生想拿它快速跑通整套流程,再换成自己的数据集做改造;另一类是写过一点 Java 图像程序、需要一份带干净数据集的参考工程。下面按我拆项目的顺序写,先看数据,再做识别,再做计数,最后是高频翻车点。

2. 三个 CSV 数据集:先确认训练集、测试集与问题集的分工

2.1 表头与前五行:拿到压缩包后第一步不是找主函数

很多同学拿到毕设压缩包,习惯先去找 xxx.java 然后点运行,跑起来报错再回头看数据。我的习惯反一下:先看 CSV。因为这类项目里,识别效果好不好,七成由数据决定,代码只是把数据里的规律挖出来。压缩包里出现三张表,命名本身就是线索:data.csv 是训练主体,datatest.csv 是拿来做验证的测试集,ques.csv 字面意思是“题目集”,也就是待预测样本。至于是不是这样,需要打印表头确认。

import java.nio.file.*; import java.nio.charset.*; import java.util.*; public class CsvProbe { public static void main(String[] args) throws Exception { String[] files = {"data.csv", "datatest.csv", "ques.csv"}; for (String file : files) { List<String> lines = Files.readAllLines( Paths.get(file), StandardCharsets.UTF_8); if (lines.isEmpty()) continue; String header = lines.get(0).replace("\uFEFF", ""); String[] cols = header.split(",", -1); System.out.println(file + " 列数=" + cols.length); System.out.println("表头: " + String.join(" | ", cols)); if (lines.size() > 1) { String firstRow = lines.get(1).trim(); System.out.println("首行: " + firstRow); } System.out.println("---"); } } }

去掉 BOM 是跟着第一个坑走的:Excel 导出的 CSV 常见带\uFEFF,不处理的话第一列列名会粘上一个隐形字符,后面按列名取值全部取不到。我一般用StandardCharsets.UTF_8先读,如果首行还是乱码,就把编码换成Charset.forName("GBK")再试,Windows 老工程十有八九是 GBK。读完先别急着写算法,把“列数、表头、首行”打印出来,贴到文本编辑器里看三张表字段是否对齐。常见字段形态有两类:一类是纯特征表,列是 id、feature1…featureN、label;另一类是带文件名的表,列是 id、filename、label,特征要自己从图片里提。这两种工程结构完全不同,判断错方向后面全是无用功。

2.2 三张表的关系怎么验证:行数、样本 ID 与图片名对齐

仅仅看表头还不够,还需要验证三张表的样本是否来自同一分布。我的做法是数行数、看第一列 ID 或文件名,做一个简单的对照。如果 data.csv 有一千行而 datatest.csv 只有两百行,通常说明后者是从前者按比例抽出来的留出集;如果两表行数非常接近,前面若干列是特征、最后一列是种类编号,那这个资源的定位就是“特征分类 + 数目统计”,不是目标检测框回归。判断依据是 label 列的值域:种类用 0/1/2 这种整数,是分类任务;如果是 x、y、w、h 四列连续值,才是检测任务,那么 fly*.jpg 就变成定位验证图,后面的思路全要调整。

我处理这类毕设包的固定动作是做一个 15 分钟的“冒烟测试”:统计每类样本数、检查是否存在全零特征列、看看 label 分布是否极度倾斜。这三个检查能提前暴露两个问题:数据不均衡导致准确率虚高,或者个别特征列全是同一个值导致归一化除零。用 Java 写一个简单统计循环就行,不用引入 pandas。如果你的机器上装了 Python,用 pandas 的df.describe()看数值分布会更直观,但项目主体是 Java 工程,我建议保留在 Java 环境里做,答辩现场演示能用同一个工程展示,不用在两种语言之间横跳。

提示:三张表字段一致时,优先把 datatest.csv 当作留出集对待,不要动它,所有调参都只基于 data.csv。

2.3 标签与图片对应关系:识别对象是几类、图片够不够

标签列是整个项目的 Ground Truth,它直接决定识别算法选型。压缩包里 fly2.jpg、fly11.jpg、fly15.jpg、fly16.jpg、fly17.jpg 这一组测试图,说明图片是按拍摄样本来的,命名不连续是因为采集时会剔除模糊帧。一个常见误用是把所有图片全部塞进训练数据,留出集就失去意义了。毕设和课设答辩最常问的就是“你的测试集从哪来”,所以必须保留 datatest.csv 对应的图片不参与训练,其他图片才进训练集。

文件命名是否和 CSV 里的文件名列一致,这一步也要核对清楚,否则后面图像读取阶段全在抛 FileNotFound。第 3 章会从图像侧讲怎么把图片和标签接上。另外,如果 label 只有两类,比如“有虫/无虫”,识别部分就是一个二分类问题,KNN、逻辑回归都够用;如果分类数到五类以上,就要重点关注特征的可分性,必要时先做一个 PCA 降维可视化,看看类簇有没有重叠。

3. 识别流程搭建:特征提取与分类,不硬上深度模型

3.1 为什么这类毕设资源通常走传统特征路线

看到图片 + CSV 的组合,很多刚入行的同学第一反应是“上 CNN”,这个判断在真实压缩包里经常是错的。原因很简单:压缩包里没有模型权重文件,数据体量也撑不起端到端训练。data.csv 通常只有几百到几千行特征,fly*.jpg 也只有几十张,这个体量根本不满足深度模型的最低要求,硬上只能得到过拟合。毕设场景下,颜色直方图、灰度和几何矩这类传统特征配合 KNN 或朴素贝叶斯,是成本最低、答辩最好解释的方案。它跑得快,CPU 就能出结果,而且每一类特征的物理意义都能说清楚,对“识别 + 数目统计”这个题目正好呼应。

3.2 灰度化与二值化:先让目标从背景里浮出来

识别之前要做图像预处理。这里的核心问题是虫子颜色和背景叶片的对比关系。常见做法是把彩色图转灰度再二值化,阈值可以用固定值或 Otsu。如果拍图环境是白色背景板,固定阈值 127 就够用;如果是在田间叶片上拍的,就需要 Otsu 自动阈值或者 HSV 通道分离。我给一个带注释的 Java 实现,用了可变阈值参数,方便后续微调。

public static BufferedImage toBinary(BufferedImage src, int threshold) { int w = src.getWidth(), h = src.getHeight(); BufferedImage dst = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int rgb = src.getRGB(x, y); int red = (rgb >> 16) & 0xFF; int green = (rgb >> 8) & 0xFF; int blue = rgb & 0xFF; int gray = (red + green + blue) / 3; dst.setRGB(x, y, gray >= threshold ? 0xFFFFFF : 0x000000); } } return dst; }

平均灰度实现简单但有个弱点:绿色背景和褐色虫子在某些光照下灰度几乎一样,阈值怎么选都分不开。我遇到这种图会改用加权灰度gray = 0.299 * red + 0.587 * green + 0.114 * blue,人眼对绿色敏感,这个系数能拉开类间差距。threshold 参数不需要一次到位,第 6 章的参数网格搜索会告诉你怎么选。二值化的目标只是把“可能是虫子的像素”标出来,不必保证像素级精度,欠分割可以在连通域阶段处理,过分割的问题留到第 4 章。

3.3 特征提取:面积、长宽比、颜色矩与直方图拼接

二值化图出来后,目标已经从背景分离,下一步是统计特征。统计连通域的面积和周长是计数的基础,而颜色特征要从原图的彩色通道里提取。我对这类项目的固定特征组是:面积、外接矩形长宽比、RGB 三通道颜色均值与方差、灰度直方图。直方图 bin 数设成 16 起步,太小类间差距不够,太大会让特征维度爆炸,影响 KNN 的速度。这些特征拼成一个向量后要归一化,否则面积动辄几千、颜色方差只有几十,距离计算会被面积一维主导。做 Min-Max 归一化比 Z-score 更适合这种场景,因为特征分布未知且可能有零方差列,Z-score 会在零方差列上除零。

public static double[] extractFeature(BufferedImage colorImg, BufferedImage binImg) { double[] res = new double[24]; // 3均值 + 3方差 + 16直方图 + 2几何 double area = countForegroundPixels(binImg); Rectangle box = boundingBox(binImg); double aspect = box.getWidth() * 1.0 / (box.getHeight() + 1e-6); double[] mean = colorMean(colorImg); double[] var = colorVar(colorImg, mean); double[] hist = grayHistogram(colorImg, 16); System.arraycopy(mean, 0, res, 0, 3); System.arraycopy(var, 0, res, 3, 3); System.arraycopy(hist, 0, res, 6, 16); res[22] = area; res[23] = aspect; return minMaxNormalize(res); }

代码里几个辅助方法都是常规实现:countForegroundPixels 统计二值图前景像素总数,boundingBox 返回最小外接矩形,colorMean 和 colorVar 分别算 RGB 三通道均值与方差,grayHistogram 输出 bin 数为 16 的灰度直方图。minMaxNormalize 的 min 和 max 必须提前从训练集计算并缓存,测试集复用同一套参数,绝不能单独对测试集再算一次。很多项目准确率虚高,根源就在归一化参数不统一。

3.4 分类器:KNN 为什么是这类题目的默认选择

特征齐了,分类器就简单了。KNN 的 K 取 3 或 5,距离度量用欧氏距离。为什么不用 SVM 或随机森林?因为毕设要求解释模型,KNN 就是“看离它最近的 k 个样本属于哪一类”,一句话讲完,而且在特征维度几十维、样本量几千的情况下,KNN 的准确率和随机森林差不了多少。如果答辩老师追问,你再解释为什么选 KNN:数据量小、维度低、便于可视化、调参只有 k 一个。k 的候选值 1、3、5、7,用 datatest.csv 跑一遍就能定。如果某个类别样本少于 5 个,k 设 5 会导致多数类霸榜,这时 k 改 3 或者加类别权重。

4. 数目统计实现:连通域标记与去重计数

4.1 识别正确不等于数得准

识别是给每个样本打标签,计数的难点至少在三个地方:同一只虫子横跨多个连通域、连续帧重复统计同一只虫子、树叶间隙造成的干扰噪声。翅脉细的虫子,在二值化后翅膀位置可能断开,一只飞蛾就裂成两个连通域,计数直接翻倍;浅色虫子在浅背景上只框住半个身体,又容易漏。数目统计这一层要解决的是把“像素连通域”合并成“目标实例”。

4.2 两遍扫描连通域标记实现

// binary[y][x]=1 视为前景像素,返回连通域个数 public static int countComponents(int[][] binary, int w, int h) { int[] parent = new int[w * h]; for (int i = 0; i < parent.length; i++) parent[i] = i; int[][] labels = new int[h][w]; int nextLabel = 1; int[][] dirs = {{-1, 0}, {0, -1}}; // 只检查左邻和上邻 for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { if (binary[y][x] == 0) continue; int left = x > 0 ? labels[y][x - 1] : 0; int top = y > 0 ? labels[y - 1][x] : 0; if (left == 0 && top == 0) { labels[y][x] = nextLabel++; } else if (left != 0 && top == 0) { labels[y][x] = left; } else if (left == 0 && top != 0) { labels[y][x] = top; } else { int a = find(parent, left); int b = find(parent, top); labels[y][x] = Math.min(a, b); parent[a] = labels[y][x]; parent[b] = labels[y][x]; } } } Set<Integer> roots = new HashSet<>(); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) if (labels[y][x] != 0) roots.add(find(parent, labels[y][x])); return roots.size(); } private static int find(int[] p, int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }

这是两遍扫描加并查集的经典实现。当当前像素的左上两个邻居属于不同连通域时,说明当前位置把两块区域连起来了,要做等价合并,否则计数时同一块区域会出现两个编号。最后通过find找出所有根标签,roots.size()就是前景目标个数。如果你更习惯用 OpenCV,connectedComponentsWithStats一行就能拿到标签和面积,但毕设代码里手写一遍标记算法反而加分,因为教材里刚好有这一章,答辩可以直接对着推导过程讲。

4.3 面积阈值与形态学修正:把噪声从计数结果里剔除

连通域标记完,噪声一般以“面积很小”和“长宽比极端”两种形态出现。灰尘噪声面积通常少于 20 像素,而真实虫子在图像里一般超过 100 像素。参数表如下:

参数含义常见取值范围调小了会怎样调大了会怎样
minArea低于此面积的连通域视为噪声50~200,按分辨率调整灰尘被计入数量小虫被漏掉
maxAspect外接矩形长宽比上限3~5细长噪声漏进计数真实细长昆虫被过滤
closeRadius形态学闭运算结构元半径2~5 像素断裂翅膀无法合并两只邻近虫子连成一体

这三个参数里 minArea 最关键。如果一张图分辨率是 640×480,虫子约占 200 像素,minArea 取 60 左右比较稳;放大到 1920×1080 后,面积整体放大五倍以上,minArea 就要抬到 300。固定阈值不调参的结果就是:小图上噪声一堆,大图上目标全丢。闭运算的 closeRadius 影响更隐蔽,它会先膨胀再腐蚀,把小于两个像素的缝隙补上;对翅膀断裂问题很有效,但若半径大于两个相邻个体间距的一半,就会把两个虫子融成一个 blob,这一步务必在第 6 章验证集调参时一并检查。

提示:调 minArea 时先连通域面积分布,把直方图的低谷作为阈值起点,别拍脑袋写 50 或 100。

4.4 计数的时间维度:多帧去重避免连续帧重复计数

如果这个毕设做的是“视频帧序列里的昆虫计数”,单帧连通域数量还不够,因为同一只虫子会连续出现在十几帧里。能扛得住答辩的简单方案是:按轨迹 IoU 做跟踪,当前帧的目标框与上一帧某个目标框的重叠面积超过 50%,就认为是同一只。每一帧在同一个轨迹 ID 上只累加一次,总数等于轨迹数而不是检测框数。没有时间维度的静态图项目,用前面 4.2 和 4.3 两节就够了,不必为了炫技硬加跟踪模块。

5. 避坑与排查:IDEA 导入、编码与路径五个高频翻车点

5.1 解压后 Module 满屏红叉

现象:用 IDEA 打开 Insect_Identification.iml 所在目录后,源码全部标红,JDK 提示找不到,整个目录没有 src 标记。

原因:.iml 文件里保存了分享者本机的 JDK 版本和输出路径,换机器后绝对路径失效;这个工程又没有 pom.xml 或 build.gradle,依赖不会自动恢复。

解决:把根目录里的*.iml和.idea文件夹先删掉,再通过 File → Project Structure → Modules → Add → Import Module 选中项目根目录,让 IDEA 以当前机器的 JDK 重新生成模块描述;然后在 Project Structure → SDK 里选本机 JDK。一般用 JDK 8 或 11 最稳,高版本 JDK 跑老式图像代码容易遇到 AWT 模块问题。之后右键源码目录,Mark as Sources Root。

5.2 CSV 读出来全是乱码

现象:表头第一列前面有“锘”字符,或者整张表的中文列名变成乱码。

原因:CSV 的编码不是 UTF-8。Windows 的 Excel 默认导出 ANSI,也就是 GBK,代码里写 UTF-8 读取自然乱码。带 BOM 的 UTF-8 读出来不会乱码,但第一列会带上隐藏字符\uFEFF,按列名匹配时会失败。

解决:先确认压缩包里有没有转码说明文件,没有的话把读取编码从 UTF_8 换成 GBK 再试,同时保留代码里的去 BOM 逻辑。用第 2 章的 CsvProbe 打印前五行,能正常输出中文说明编码对了;继续乱码就换编码继续试。即使后面要做特征归一化,也要先保证列名能正确匹配,否则代码会在读写字段时直接抛异常。

5.3 图片 FileNotFound:路径与工作目录不一致

现象:程序一运行就在 fly11.jpg 的读取处抛 FileNotFoundException,但文件明明在工程目录里。

原因:代码里写的是相对路径new File("fly11.jpg"),而 IDEA 运行时工作目录默认指向打开工程的顶层,如果工程结构里还有一层 src 或其他子目录,相对路径就失效了。Windows 路径分隔符用反斜杠也容易在跨平台时出问题。

解决:在 Run Configuration 里把 Working directory 改成$MODULE_DIR$,或者在代码里用全局常量拼路径new File("images/fly11.jpg").getCanonicalFile()。更稳的做法是启动时接受一个-Dimg.dir=...参数,数据路径和代码路径分离,换数据集时不用改代码直接改启动参数。我的习惯是永远不在代码里写死图片文件名,至少用一个配置常量。

5.4 分类准确率约等于随机猜测

现象:datatest.csv 上测试准确率只有 0.5 左右,和丢硬币一样。

原因:多数情况不是算法错了,是特征没归一化。面积量纲是几千,颜色均值只有几十到两百,欧氏距离计算时面积一维贡献了 90% 的权重,其他维度形同虚设;另一种可能是训练集和测试集的归一化参数各自独立计算,导致特征空间不对齐。

解决:训练集计算 min 和 max 并保存成归一化参数,测试集复用同一套变换;检查每个特征列的方差,方差极小的列直接丢弃。改完这两个点,准确率通常会有肉眼可见的提升。如果还不行,再回头怀疑标签对齐或特征选择,不要急着换分类器。

5.5 计数把同一只虫子算成两个连通域

现象:肉眼看到图里只有 3 只虫,程序输出 5;或者统计数比真实数翻倍。

原因:最常见是二值化阈值不佳,导致虫身内部出现小的暗区,连通域断裂;其次是 4.3 里的 minArea 太小,翅膀旁边的碎屑也被算成独立目标。

解决:先看分割输出图,对二值图做闭运算再标记;如果阈值效果不稳定,改成 Otsu 自动阈值而不是写死 127;最后用面积阈值过滤连通域。计数结果要对齐第 4 章的参数表,不能只看最终数字,要把中间二值图和标记结果可视化出来对比,不然永远不知道是漏了还是多了。

6. 验证技巧:用 datatest.csv 做留出验证与参数微调

6.1 参数网格:minArea、归一化与 K 值一起调

调参不是一个参数一个参数试,那样参数之间会互相干扰。我的做法是把 minArea、灰度直方图 bin 数、K 值三个核心参数做成小网格,用 datatest.csv 评估所有组合,选准确率最高且计数误差最小的一组。minArea 影响单图计数误差,bin 数影响特征可分性,K 值影响分类稳定性,三者调一次就够了。

参数调小会怎样调大会怎样建议起始值
minArea灰尘混入计数偏高小目标漏检按面积直方图低谷选
bin直方图过细类内抖动大过粗类间差异被抹平16
K过拟合单样本噪声边界样本被多数类带偏3

6.2 固定随机种子与留出集顺序

最后,把 datatest.csv 当留出集,数据切分要固定随机种子,Java 里用Random random = new Random(42),保证每次跑结果一致。如果 data.csv 和 datatest.csv 字段分布一致,可以在 data 上随机抽 20% 与 datatest 合并为验证集;如果 datatest 只有特征没有标签,就只用 data.csv 内部切分验证。整个流程从表头探查到参数网格,一次走完不超过 20 分钟。从那以后我每次拿毕设压缩包,都会先去打印表头、数类别数、核对图片和 ID 对应关系,再跑这套留出验证。这个顺序至少帮我在三个项目里避免了“答辩现场才发现归一化参数不一致”的尴尬。数据、代码骨架、测试图一次给齐,你只需要把参数和特征调成符合自己场景的样子,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询