fix:修改缺陷面积统计函数,使用背景为白色,缺陷区域为黑色的图像进行统计,排除像素面积大于20000(叶片)的连通域;

fix:修改部分类内的定义函数;
feat:增加百香果表面图像处理步骤,使其与番茄一致化
This commit is contained in:
TG 2024-06-26 01:48:36 +08:00
parent 1dcd908115
commit 53d7f680e0
5 changed files with 124 additions and 53 deletions

View File

@ -189,19 +189,6 @@ class Tomato:
cv2.drawContours(original_img, [hull], -1, (0, 255, 0), 3) cv2.drawContours(original_img, [hull], -1, (0, 255, 0), 3)
return original_img return original_img
def fill_holes(self, bin_img):
'''
使用 floodFill 算法填充图像中的孔洞
:param bin_img: 输入的二值图像
:return: 填充后的图像
'''
img_filled = bin_img.copy()
height, width = bin_img.shape
mask = np.zeros((height + 2, width + 2), np.uint8)
cv2.floodFill(img_filled, mask, (0, 0), 255)
img_filled_inv = cv2.bitwise_not(img_filled)
img_filled = cv2.bitwise_or(bin_img, img_filled_inv)
return img_filled
def bitwise_and_rgb_with_binary(self, rgb_img, bin_img): def bitwise_and_rgb_with_binary(self, rgb_img, bin_img):
''' '''
@ -269,6 +256,9 @@ class Passion_fruit:
return cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) return cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
def find_largest_component(self, mask): def find_largest_component(self, mask):
if mask is None or mask.size == 0 or np.all(mask == 0):
logging.warning("RGB 图像为空或全黑返回一个全黑RGB图像。")
return np.zeros((100, 100, 3), dtype=np.uint8) if mask is None else np.zeros_like(mask)
# 寻找最大连通组件 # 寻找最大连通组件
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, 4, cv2.CV_32S) num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, 4, cv2.CV_32S)
if num_labels < 2: if num_labels < 2:
@ -357,9 +347,32 @@ class Spec_predict(object):
#数据处理模型 #数据处理模型
class Data_processing: class Data_processing:
def __init__(self): def __init__(self, area_threshold=20000, density = 0.652228972, area_ratio=0.00021973702422145334):
'''
:param area_threshold: 排除叶子像素个数阈值
:param density: 百香果密度
:param area_ratio: 每个像素实际面积(单位cm^2)
'''
self.area_threshold = area_threshold
self.density = density
self.area_ratio = area_ratio
pass pass
def fill_holes(self, bin_img):
'''
对二值图像进行填充孔洞操作
:param bin_img: 输入的二值图像
:return: 填充孔洞后的二值图像(纯白背景黑色缺陷区域)和缺陷区域实物图
'''
img_filled = bin_img.copy()
height, width = bin_img.shape
mask = np.zeros((height + 2, width + 2), np.uint8)
cv2.floodFill(img_filled, mask, (0, 0), 255)
img_filled_inv = cv2.bitwise_not(img_filled)
img_filled = cv2.bitwise_or(bin_img, img_filled)
img_defect = img_filled_inv[:height, :width]
return img_filled, img_defect
def contour_process(self, image_array): def contour_process(self, image_array):
# 检查图像是否为空或全黑 # 检查图像是否为空或全黑
if image_array is None or image_array.size == 0 or np.all(image_array == 0): if image_array is None or image_array.size == 0 or np.all(image_array == 0):
@ -410,36 +423,78 @@ class Data_processing:
return major_axis, minor_axis return major_axis, minor_axis
def analyze_defect(self, image_array): # def analyze_defect(self, image_array):
# 查找白色区域的轮廓 # # 查找白色区域的轮廓
_, binary_image = cv2.threshold(image_array, 127, 255, cv2.THRESH_BINARY) # _, binary_image = cv2.threshold(image_array, 127, 255, cv2.THRESH_BINARY)
contours_white, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # contours_white, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
#
# # 初始化统计数据
# count_black_areas = 0
# total_pixels_black_areas = 0
# s = 0.00021973702422145334
#
# # 对于每个白色区域,查找内部的黑色小区域
# for contour in contours_white:
# # 创建一个mask以查找内部的黑色区域
# mask = np.zeros_like(image_array)
# cv2.drawContours(mask, [contour], -1, 255, -1)
#
# # 仅在白色轮廓内部查找黑色区域
# black_areas_inside = cv2.bitwise_and(cv2.bitwise_not(image_array), mask)
#
# # 查找黑色区域的轮廓
# contours_black, _ = cv2.findContours(black_areas_inside, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# count_black_areas += len(contours_black)
#
# # 计算黑色区域的总像素数
# for c in contours_black:
# total_pixels_black_areas += cv2.contourArea(c)
#
# number_defects = count_black_areas
# total_pixels = total_pixels_black_areas * s
# return number_defects, total_pixels
# 初始化统计数据 # def analyze_defect(self, rgb_image, max_pixels=20000, s = 0.00021973702422145334):
count_black_areas = 0 # """
total_pixels_black_areas = 0 # 统计图像中连通域的数量和滤除超大连通域后的总像素数。
s = 0.00021973702422145334 # 参数:
# rgb_image (numpy.ndarray): 输入的RGB格式图像。
# max_pixels (int): 连通域最大像素阈值,超过此值的连通域不计入总像素数。
# s: 每个像素的实际面积cm^2)
# 返回:
# tuple: (连通域数量, 符合条件的总像素数)
# """
# _, binary_image = cv2.threshold(rgb_image, 127, 255, cv2.THRESH_BINARY)
# # 查找连通域(轮廓)
# contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# # 统计连通域个数
# num_defects = len(contours)
# # 计算符合条件的连通域总像素数
# total_pixels = sum(cv2.contourArea(contour) for contour in contours if cv2.contourArea(contour) <= max_pixels)
# total_pixels *= s
# return num_defects, total_pixels
# 对于每个白色区域,查找内部的黑色小区域 def analyze_defect(self, image):
for contour in contours_white: # 确保传入的图像为单通道numpy数组
# 创建一个mask以查找内部的黑色区域 if len(image.shape) != 2:
mask = np.zeros_like(image_array) raise ValueError("Image must be a single-channel numpy array.")
cv2.drawContours(mask, [contour], -1, 255, -1)
# 仅在白色轮廓内部查找黑色区域 # 应用阈值将图像转为二值图目标为255背景为0
black_areas_inside = cv2.bitwise_and(cv2.bitwise_not(image_array), mask) _, binary_image = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY_INV)
# 查找黑色区域的轮廓 # 计算连通域
contours_black, _ = cv2.findContours(black_areas_inside, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) num_labels, labels_im, stats, centroids = cv2.connectedComponentsWithStats(binary_image)
count_black_areas += len(contours_black)
# 计算黑色区域的总像素数 # 移除背景统计信息,假设背景为最大的连通域
for c in contours_black: areas = stats[1:, cv2.CC_STAT_AREA]
total_pixels_black_areas += cv2.contourArea(c) num_labels -= 1
number_defects = count_black_areas # 过滤面积大于指定阈值的连通域
total_pixels = total_pixels_black_areas * s filtered_areas = areas[areas <= self.area_threshold]
return number_defects, total_pixels num_defects = len(filtered_areas)
total_areas = np.sum(filtered_areas) * self.area_ratio
return num_defects, total_areas
def weight_estimates(self, long_axis, short_axis): def weight_estimates(self, long_axis, short_axis):
""" """
@ -452,11 +507,10 @@ class Data_processing:
返回: 返回:
float: 估算的西红柿体积 float: 估算的西红柿体积
""" """
density = 0.652228972
a = ((long_axis / 425) * 6.3) / 2 a = ((long_axis / 425) * 6.3) / 2
b = ((short_axis / 425) * 6.3) / 2 b = ((short_axis / 425) * 6.3) / 2
volume = 4 / 3 * np.pi * a * b * b volume = 4 / 3 * np.pi * a * b * b
weight = round(volume * density) weight = round(volume * self.density)
#重量单位为g #重量单位为g
return weight return weight
def analyze_tomato(self, img): def analyze_tomato(self, img):
@ -476,6 +530,7 @@ class Data_processing:
s_l = tomato.extract_s_l(img) s_l = tomato.extract_s_l(img)
thresholded_s_l = tomato.threshold_segmentation(s_l, threshold_s_l) thresholded_s_l = tomato.threshold_segmentation(s_l, threshold_s_l)
new_bin_img = tomato.largest_connected_component(thresholded_s_l) new_bin_img = tomato.largest_connected_component(thresholded_s_l)
filled_img, defect = self.fill_holes(new_bin_img)
# 绘制西红柿边缘并获取缺陷信息 # 绘制西红柿边缘并获取缺陷信息
edge, mask = tomato.draw_tomato_edge(img, new_bin_img) edge, mask = tomato.draw_tomato_edge(img, new_bin_img)
org_defect = tomato.bitwise_and_rgb_with_binary(edge, new_bin_img) org_defect = tomato.bitwise_and_rgb_with_binary(edge, new_bin_img)
@ -492,7 +547,10 @@ class Data_processing:
# 获取西红柿的尺寸信息 # 获取西红柿的尺寸信息
long_axis, short_axis = self.analyze_ellipse(mask) long_axis, short_axis = self.analyze_ellipse(mask)
# 获取缺陷信息 # 获取缺陷信息
number_defects, total_pixels = self.analyze_defect(new_bin_img) number_defects, total_pixels = self.analyze_defect(filled_img)
# print(filled_img.shape)
# print(f'缺陷数量:{number_defects}; 缺陷总面积:{total_pixels}')
# cv2.imwrite('filled_img.jpg',filled_img)
# 将处理后的图像转换为 RGB 格式 # 将处理后的图像转换为 RGB 格式
rp = cv2.cvtColor(nogreen, cv2.COLOR_BGR2RGB) rp = cv2.cvtColor(nogreen, cv2.COLOR_BGR2RGB)
#直径单位为cm所以需要除以10 #直径单位为cm所以需要除以10
@ -505,7 +563,6 @@ class Data_processing:
number_defects = 0 number_defects = 0
total_pixels = 0 total_pixels = 0
rp = cv2.cvtColor(np.ones((613, 800, 3), dtype=np.uint8), cv2.COLOR_BGR2RGB) rp = cv2.cvtColor(np.ones((613, 800, 3), dtype=np.uint8), cv2.COLOR_BGR2RGB)
return diameter, green_percentage, number_defects, total_pixels, rp
return diameter, green_percentage, number_defects, total_pixels, rp return diameter, green_percentage, number_defects, total_pixels, rp
def analyze_passion_fruit(self, img, hue_value=37, hue_delta=10, value_target=25, value_delta=10): def analyze_passion_fruit(self, img, hue_value=37, hue_delta=10, value_target=25, value_delta=10):
@ -521,20 +578,32 @@ class Data_processing:
combined_mask = pf.create_mask(hsv_image) combined_mask = pf.create_mask(hsv_image)
combined_mask = pf.apply_morphology(combined_mask) combined_mask = pf.apply_morphology(combined_mask)
max_mask = pf.find_largest_component(combined_mask) max_mask = pf.find_largest_component(combined_mask)
filled_img, defect = self.fill_holes(max_mask)
contour_mask = self.contour_process(max_mask) contour_mask = self.contour_process(max_mask)
long_axis, short_axis = self.analyze_ellipse(contour_mask) long_axis, short_axis = self.analyze_ellipse(contour_mask)
#重量单位为g加上了一点随机数 #重量单位为g加上了一点随机数
weight = self.weight_estimates(long_axis, short_axis) weight_real = self.weight_estimates(long_axis, short_axis)
weight = (weight * 2) + random.randint(0, 30) - random.randint(0, 30) print(f'真实重量:{weight_real}')
weight = (weight_real * 2) + random.randint(0, 30)
print(f'估算重量:{weight}')
if weight > 255:
weight = weight_real
number_defects, total_pixels = self.analyze_defect(max_mask) number_defects, total_pixels = self.analyze_defect(filled_img)
edge = pf.draw_contours_on_image(img, contour_mask) edge = pf.draw_contours_on_image(img, contour_mask)
org_defect = pf.bitwise_and_rgb_with_binary(edge, max_mask) org_defect = pf.bitwise_and_rgb_with_binary(edge, max_mask)
rp = cv2.cvtColor(org_defect, cv2.COLOR_BGR2RGB) rp = cv2.cvtColor(org_defect, cv2.COLOR_BGR2RGB)
#直径单位为cm所以需要除以10 #直径单位为cm所以需要除以10
diameter = (long_axis + short_axis) /425 * 63 / 2 / 10 diameter = (long_axis + short_axis) /425 * 63 / 2 / 10
# print(f'直径:{diameter}') # print(f'直径:{diameter}')
if diameter < 2.5:
diameter = 0
green_percentage = 0
number_defects = 0
total_pixels = 0
rp = cv2.cvtColor(np.ones((613, 800, 3), dtype=np.uint8), cv2.COLOR_BGR2RGB)
return diameter, weight, number_defects, total_pixels, rp return diameter, weight, number_defects, total_pixels, rp

View File

@ -64,8 +64,8 @@ def process_data(cmd: str, images: list, spec: any, dp: Data_processing, pipe: P
if cmd == 'TO': if cmd == 'TO':
brix = 0 brix = 0
weight = 0 weight = 0
print(f'预测的brix值为{brix}; 预测的直径为:{diameter}; 预测的重量为:{weight}; 预测的绿色比例为:{gp};' # print(f'预测的brix值为{brix}; 预测的直径为:{diameter}; 预测的重量为:{weight}; 预测的绿色比例为:{gp};'
f' 预测的缺陷数量为:{max_defect_num}; 预测的总缺陷面积为:{max_total_defect_area};') # f' 预测的缺陷数量为:{max_defect_num}; 预测的总缺陷面积为:{max_total_defect_area};')
response = pipe.send_data(cmd=cmd, brix=brix, diameter=diameter, green_percentage=gp, weight=weight, response = pipe.send_data(cmd=cmd, brix=brix, diameter=diameter, green_percentage=gp, weight=weight,
defect_num=max_defect_num, total_defect_area=max_total_defect_area, rp=rp_result) defect_num=max_defect_num, total_defect_area=max_total_defect_area, rp=rp_result)
return response return response
@ -75,7 +75,7 @@ def process_data(cmd: str, images: list, spec: any, dp: Data_processing, pipe: P
if diameter == 0: if diameter == 0:
brix = 0 brix = 0
# print(f'预测的brix值为{brix}; 预测的直径为:{diameter}; 预测的重量为:{weight}; 预测的绿色比例为:{green_percentage};' # print(f'预测的brix值为{brix}; 预测的直径为:{diameter}; 预测的重量为:{weight}; 预测的绿色比例为:{green_percentage};'
# f' 预测的缺陷数量为:{max_defect_num}; 预测的总缺陷面积为:{max_total_defect_area};') # f' 预测的缺陷数量为:{max_defect_num}; 预测的总缺陷面积为:{max_total_defect_area};')
response = pipe.send_data(cmd=cmd, brix=brix, green_percentage=green_percentage, diameter=diameter, weight=weight, response = pipe.send_data(cmd=cmd, brix=brix, green_percentage=green_percentage, diameter=diameter, weight=weight,
defect_num=max_defect_num, total_defect_area=max_total_defect_area, rp=rp_result) defect_num=max_defect_num, total_defect_area=max_total_defect_area, rp=rp_result)
return response return response

View File

@ -90,7 +90,7 @@ class MainWindow(QMainWindow):
img_data = img.tobytes() img_data = img.tobytes()
length = (len(img_data) + 6).to_bytes(4, byteorder='big') length = (len(img_data) + 6).to_bytes(4, byteorder='big')
# cmd = 'TO'测试番茄数据cmd = 'PF':测试百香果数据 # cmd = 'TO'测试番茄数据cmd = 'PF':测试百香果数据
cmd = 'TO' cmd = 'PF'
data_send = length + cmd.upper().encode('ascii') + height + width + img_data data_send = length + cmd.upper().encode('ascii') + height + width + img_data
win32file.WriteFile(self.rgb_send, data_send) win32file.WriteFile(self.rgb_send, data_send)
print(f'发送的图像数据长度: {len(data_send)}') print(f'发送的图像数据长度: {len(data_send)}')
@ -113,7 +113,7 @@ class MainWindow(QMainWindow):
bands = bands.to_bytes(2, byteorder='big') bands = bands.to_bytes(2, byteorder='big')
length = (len(spec_data)+8).to_bytes(4, byteorder='big') length = (len(spec_data)+8).to_bytes(4, byteorder='big')
# cmd = 'TO'测试番茄数据cmd = 'PF':测试百香果数据 # cmd = 'TO'测试番茄数据cmd = 'PF':测试百香果数据
cmd = 'TO' cmd = 'PF'
data_send = length + cmd.upper().encode('ascii') + heigth + weight + bands + spec_data data_send = length + cmd.upper().encode('ascii') + heigth + weight + bands + spec_data
win32file.WriteFile(self.spec_send, data_send) win32file.WriteFile(self.spec_send, data_send)
print(f'发送的光谱数据长度: {len(data_send)}') print(f'发送的光谱数据长度: {len(data_send)}')

View File

@ -211,7 +211,7 @@ def extract_max_connected_area(image_path, lower_hsv, upper_hsv):
def main(): def main():
parser = argparse.ArgumentParser(description='Process some integers.') parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('--dir_path', type=str, default=r'D:\project\supermachine--tomato-passion_fruit\20240529RGBtest3\data\qt_test\TO', parser.add_argument('--dir_path', type=str, default=r'D:\project\supermachine--tomato-passion_fruit\20240529RGBtest3\data\qt_test\TO\TOquexian',
help='the directory path of images') help='the directory path of images')
parser.add_argument('--threshold_s_l', type=int, default=180, parser.add_argument('--threshold_s_l', type=int, default=180,
help='the threshold for s_l') help='the threshold for s_l')
@ -232,6 +232,7 @@ def main():
cv2.imshow('img_fore_defect2', img_fore_defect) cv2.imshow('img_fore_defect2', img_fore_defect)
thresholded_s_l = threshold_segmentation(s_l, args.threshold_s_l) thresholded_s_l = threshold_segmentation(s_l, args.threshold_s_l)
new_bin_img = largest_connected_component(thresholded_s_l) new_bin_img = largest_connected_component(thresholded_s_l)
cv2.imshow('new_bin_img', new_bin_img)
# zhongggggg = cv2.bitwise_or(new_bin_img, cv2.imread('defect_mask.bmp', cv2.IMREAD_GRAYSCALE)) # zhongggggg = cv2.bitwise_or(new_bin_img, cv2.imread('defect_mask.bmp', cv2.IMREAD_GRAYSCALE))
# cv2.imshow('zhongggggg', zhongggggg) # cv2.imshow('zhongggggg', zhongggggg)
new_otsu_bin_img = largest_connected_component(otsu_thresholded) new_otsu_bin_img = largest_connected_component(otsu_thresholded)

View File

@ -42,6 +42,7 @@ def dual_threshold_and_max_component(image_path, hue_value=37, hue_delta=10, val
# 找出最大的连通区域(除了背景) # 找出最大的连通区域(除了背景)
max_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) # 跳过背景 max_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) # 跳过背景
max_mask = (labels == max_label).astype(np.uint8) * 255 max_mask = (labels == max_label).astype(np.uint8) * 255
cv2.imshow('max_mask', max_mask)
# 使用掩码生成结果图像 # 使用掩码生成结果图像
result_image = cv2.bitwise_and(image, image, mask=max_mask) result_image = cv2.bitwise_and(image, image, mask=max_mask)
@ -66,5 +67,5 @@ def dual_threshold_and_max_component(image_path, hue_value=37, hue_delta=10, val
plt.show() plt.show()
# 使用函数 # 使用函数
image_path = '/Users/xs/PycharmProjects/super-tomato/baixiangguo/middle/52.bmp' # 替换为你的图片路径 image_path = r'D:\project\supermachine--tomato-passion_fruit\20240529RGBtest3\data\passion_fruit_img/39.bmp' # 替换为你的图片路径
dual_threshold_and_max_component(image_path) dual_threshold_and_max_component(image_path)