用opencv-python 实现菌群计数
这两天在公司接个活,干完了简单记录一下,写的不好也算是个抛砖引玉。
需求很简单,微生物的同事最近有标定任务,每天大概都要统计150个培养皿上的菌群数,大概要持续半个月,领导考虑让我写个小程序帮助他们计数。
先简单分析一下需求:
- 只是公司内部人用而且就用半个月,所以没有必要用c艹,用python写个脚本又快又方便。
- 微生物的同事不熟悉命令行所以还是需要做一个图形界面
综上,决定用opencv+pyqt写个脚本来完成任务
图片都很理想,如下图所示
计数思路也很简单:
- 转换成灰度图
- 二值化(不同菌群不同光照环境都会影响阈值,所以我没有在代码里写死阈值,而是在界面留了可以调节阈值的控件)
- 对图像进行膨胀腐蚀(如果图片理想并且只计数就不需要)
- 连通区域得到轮廓(因为菌群是圆形所以也可以用霍夫圆检测,但是我这里只写了根据灰度值的检测方法)
- 进行计数
但是实际过程中发现有下列问题:
- 按灰度值二值化时培养皿周围因为反光会影响,解决方案是围着培养皿画一个圈,然后只识别圈里面的,外面的人工数(也不会太多,最多二十多个)。
- 有的菌群见挨得太近,用联通区域的方法会把两个菌群识别成一个,而且因为两个菌群连接部分太宽不能通过开运算来改善解决方案是在界面把轮廓显示出来然后人工校对(详见代码)
- 关于第二点我还有一种想法就是用霍夫圆检测去匹配菌群来做检测,但是因为我对霍夫圆检测还不太熟练而且这个催的又急所以暂时搁置下来,
import sys
import cv2 as cv
import os
import numpy as np
from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog
from ui import *
class MainWindow(QDialog, Ui_Form):
def __init__(self, parent=None):
super(MainWindow, self).__init__()
self.setupUi(self)
# 初始化统计用变量
self.path_list = []
self.path_list_i = 0
self.count = 0
self.bias_sum = 0
# 定义控制参数
self.CENTER_X = 900
self.CENTER_Y = 900
self.RADIUS = 680
# 定义图像参数
self.THRESH = 170
self.MAXVAL = 255
# 初始化辅助线条
self.left_line = 600
self.right_line = 1200
self.up_line = 600
self.down_line = 1200
# 初始化信号与槽连接
##初始化打开文件操作按钮
self.open_folder_button.clicked.connect(self.get_file_path)
self.next_button.clicked.connect(self.press_next)
self.pre_button.clicked.connect(self.press_pre)
## 初始化灰度值调节连接
self.min_spin.valueChanged.connect(self.change_min_grayscale_spin)
self.max_spin.valueChanged.connect(self.change_max_grayscale_spin)
self.min_slider.valueChanged.connect(self.change_min_grayscale_slider)
self.max_slider.valueChanged.connect(self.change_max_grayscale_slider)
## 初始化辅助线,mask连接
self.left_spin.valueChanged.connect(self.change_left)
self.right_spin.valueChanged.connect(self.change_right)
self.up_spin.valueChanged.connect(self.change_up)
self.down_spin.valueChanged.connect(self.change_down)
self.center_x_spin.valueChanged.connect(self.change_center_x)
self.center_y_spin.valueChanged.connect(self.change_center_y)
self.r_spin.valueChanged.connect(self.change_r)
##初始化九个偏置量连接
self.bias_1.valueChanged.connect(self.bias_change)
self.bias_2.valueChanged.connect(self.bias_change)
self.bias_3.valueChanged.connect(self.bias_change)
self.bias_4.valueChanged.connect(self.bias_change)
self.bias_5.valueChanged.connect(self.bias_change)
self.bias_6.valueChanged.connect(self.bias_change)
self.bias_7.valueChanged.connect(self.bias_change)
self.bias_8.valueChanged.connect(self.bias_change)
self.bias_9.valueChanged.connect(self.bias_change)
def get_file_path(self):
dir_choose = QFileDialog.getExistingDirectory(self, "选取文件夹", os.getcwd())
temp_list = os.listdir(dir_choose)
for i in temp_list:
self.path_list.append(dir_choose + "/" + i)
self.path_list_i = 0
self.shulaibao()
def press_next(self):
if self.path_list_i == len(self.path_list) - 1:
pass
else:
self.path_list_i += 1
self.shulaibao()
def press_pre(self):
if self.path_list_i == 0:
pass
else:
self.path_list_i -= 1
self.shulaibao()
def change_min_grayscale_slider(self):
self.THRESH = self.min_slider.value()
self.min_spin.setValue(self.THRESH)
self.shulaibao()
def change_min_grayscale_spin(self):
self.THRESH = self.min_spin.value()
self.min_slider.setValue(self.THRESH)
self.shulaibao()
def change_max_grayscale_slider(self):
self.MAXVAL = self.max_slider.value()
self.max_spin.setValue(self.MAXVAL)
self.shulaibao()
def change_max_grayscale_spin(self):
self.MAXVAL = self.max_spin.value()
self.max_slider.setValue(self.MAXVAL)
self.shulaibao()
def change_center_x(self):
self.CENTER_X = self.center_x_spin.value()
self.shulaibao()
def change_center_y(self):
self.CENTER_Y = self.center_y_spin.value()
self.shulaibao()
def change_r(self):
self.RADIUS = self.r_spin.value()
self.shulaibao()
def change_left(self):
self.left_line = self.left_spin.value()
self.shulaibao()
def change_right(self):
self.right_line = self.right_spin.value()
self.shulaibao()
def change_down(self):
self.right_line = self.right_spin.value()
self.shulaibao()
def change_up(self):
self.right_line = self.right_spin.value()
self.shulaibao()
def bias_change(self):
self.bias_sum = self.bias_1.value() + self.bias_2.value() + self.bias_3.value() + self.bias_4.value() + \
self.bias_5.value() + self.bias_6.value() + self.bias_7.value() + self.bias_8.value() + \
self.bias_9.value()
self.count_label.setText(f"总数为:{self.count + self.bias_sum}")
def shulaibao(self):
cv.destroyAllWindows()
img = cv.imread(self.path_list[self.path_list_i])
img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
img_gray = cv.blur(img_gray, (3, 3))
# 绘制圆形mask,并与灰度图合成目标图
mask = np.zeros_like(img_gray)
mask = cv.circle(mask, (self.CENTER_X, self.CENTER_Y), self.RADIUS, (255, 255, 255), -1)
mask = mask // 255
img_with_mask = mask * img_gray
# 二值化处理
_, img_threshold = cv.threshold(img_with_mask, self.THRESH, self.MAXVAL, cv.THRESH_BINARY)
# 连通区域获得边框
contours, _ = cv.findContours(img_threshold, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
img_1 = cv.drawContours(img, contours, -1, (0, 0, 255))
img_1 = cv.circle(img_1, (self.CENTER_X, self.CENTER_Y), self.RADIUS, (0, 255, 0), 5)
img_1 = cv.putText(img_1, "{}".format(len(contours)), (100, 100), cv.FONT_HERSHEY_SIMPLEX, 2, (255, 255, 255),5)
# 绘制辅助线
cv.line(img_1, (self.left_line, 0), (self.left_line, 1800), (0, 255, 0), 3, 8)
cv.line(img_1, (self.right_line, 0), (self.right_line, 1800), (0, 255, 0), 3, 8)
cv.line(img_1, (0, self.up_line), (1800, self.up_line), (0, 255, 0), 3, 8)
cv.line(img_1, (0, self.down_line), (1800, self.down_line), (0, 255, 0), 3, 8)
# 显示图片,记录数目
img_1 = cv.resize(img_1, (0, 0), fx=0.5, fy=0.5)
self.count = len(contours)
self.bias_change()
cv.imshow("COUNT", img_1)
cv.waitKey(0)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
cv.destroyAllWindows()
sys.exit()
最后界面如图
结果如下图
可以看到辅助线分成九个区域,工作人员可以在界面上九个区域里分别输入没有识别出来的数目,会自动加和显示在总数哪里