diff --git a/.gitignore b/.gitignore index d698033..d431beb 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,8 @@ fabric.properties !/20240410RGBtest1/super-tomato/defect_mask.bmp !/20240410RGBtest1/super-tomato/prediction.png /20240529RGBtest3/data/ +/20240627Actual_deployed/.idea/ +/20240627Actual_deployed/qt_test/ +/20240627Actual_deployed/封装exe/ +/20240627Actual_deployed/qt_test/ +/20240627Actual_deployed/qt_test/PF/ diff --git a/20240627Actual_deployed/classifer.py b/20240627Actual_deployed/classifer.py new file mode 100644 index 0000000..e3b9a1e --- /dev/null +++ b/20240627Actual_deployed/classifer.py @@ -0,0 +1,658 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/6/4 21:34 +# @Author : GG +# @File : classifer.py +# @Software: PyCharm + +import os +import cv2 +import utils +import joblib +import logging +import random +import numpy as np +from utils import Pipe +from config import Config as setting +from sklearn.ensemble import RandomForestRegressor + +#番茄RGB处理模型 +class Tomato: + def __init__(self, find_reflection_threshold=setting.find_reflection_threshold, extract_g_r_factor=setting.extract_g_r_factor): + ''' 初始化 Tomato 类。''' + self.find_reflection_threshold = find_reflection_threshold + self.extract_g_r_factor = extract_g_r_factor + pass + + def extract_s_l(self, image): + ''' + 提取图像的 S 通道(饱和度)和 L 通道(亮度),并将两者相加。 + :param image: 输入的 BGR 图像 + :return: S 通道和 L 通道相加的结果 + ''' + hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) + lab = cv2.cvtColor(image, cv2.COLOR_BGR2Lab) + s_channel = hsv[:, :, 1] + l_channel = lab[:, :, 0] + result = cv2.add(s_channel, l_channel) + return result + + def find_reflection(self, image): + ''' + 通过阈值处理识别图像中的反射区域。 + :param image: 输入的单通道图像 + :param threshold: 用于二值化的阈值 + :return: 二值化后的图像,高于阈值的部分为白色,其余为黑色 + ''' + _, reflection = cv2.threshold(image, self.find_reflection_threshold, 255, cv2.THRESH_BINARY) + return reflection + + def otsu_threshold(self, image): + ''' + 使用 Otsu 大津法自动计算并应用阈值,进行图像的二值化处理。 + :param image: 输入的单通道图像 + :return: 二值化后的图像 + ''' + _, binary = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + return binary + + def extract_g_r(self, image): + ''' + 提取图像中的 G 通道(绿色),放大并减去 R 通道(红色)。 + :param image: 输入的 BGR 图像 + :return: G 通道乘以 1.5 后减去 R 通道的结果 + ''' + g_channel = image[:, :, 1] + r_channel = image[:, :, 2] + result = cv2.subtract(cv2.multiply(g_channel, self.extract_g_r_factor), r_channel) + return result + + def extract_r_b(self, image): + ''' + 提取图像中的 R 通道(红色)和 B 通道(蓝色),并进行相减。 + :param image: 输入的 BGR 图像 + :return: R 通道减去 B 通道的结果 + ''' + r_channel = image[:, :, 2] + b_channel = image[:, :, 0] + result = cv2.subtract(r_channel, b_channel) + return result + + def extract_r_g(self, image): + ''' + 提取图像中的 R 通道(红色)和 G 通道(绿色),并进行相减。 + :param image: 输入的 BGR 图像 + :return: R 通道减去 G 通道的结果 + ''' + r_channel = image[:, :, 2] + g_channel = image[:, :, 1] + result = cv2.subtract(r_channel, g_channel) + return result + + def threshold_segmentation(self, image, threshold, color=255): + ''' + 对图像进行阈值分割,高于阈值的部分设置为指定的颜色。 + :param image: 输入的单通道图像 + :param threshold: 阈值 + :param color: 设置的颜色值 + :return: 分割后的二值化图像 + ''' + _, result = cv2.threshold(image, threshold, color, cv2.THRESH_BINARY) + return result + + def bitwise_operation(self, image1, image2, operation='and'): + ''' + 对两幅图像执行位运算(与或运算)。 + :param image1: 第一幅图像 + :param image2: 第二幅图像 + :param operation: 执行的操作类型('and' 或 'or') + :return: 位运算后的结果 + ''' + if operation == 'and': + result = cv2.bitwise_and(image1, image2) + elif operation == 'or': + result = cv2.bitwise_or(image1, image2) + else: + raise ValueError("operation must be 'and' or 'or'") + return result + + def largest_connected_component(self, bin_img): + ''' + 提取二值图像中的最大连通区域。 + :param bin_img: 输入的二值图像 + :return: 只包含最大连通区域的二值图像 + ''' + num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(bin_img, connectivity=8) + if num_labels <= 1: + return np.zeros_like(bin_img) + largest_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) + new_bin_img = np.zeros_like(bin_img) + new_bin_img[labels == largest_label] = 255 + return new_bin_img + + def close_operation(self, bin_img, kernel_size=(5, 5)): + ''' + 对二值图像进行闭运算,用于消除内部小孔和连接接近的对象。 + :param bin_img: 输入的二值图像 + :param kernel_size: 核的大小 + :return: 进行闭运算后的图像 + ''' + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, kernel_size) + closed_img = cv2.morphologyEx(bin_img, cv2.MORPH_CLOSE, kernel) + return closed_img + + def open_operation(self, bin_img, kernel_size=(5, 5)): + ''' + 对二值图像进行开运算,用于去除小的噪点。 + :param bin_img: 输入的二值图像 + :param kernel_size: 核的大小 + :return: 进行开运算后的图像 + ''' + kernel = cv2.getStructuringElement(cv2.MORPH_RECT, kernel_size) + opened_img = cv2.morphologyEx(bin_img, cv2.MORPH_OPEN, kernel) + return opened_img + + def draw_tomato_edge(self, original_img, bin_img): + ''' + 在原始图像上绘制最大西红柿轮廓的近似多边形。 + :param original_img: 原始 BGR 图像 + :param bin_img: 西红柿的二值图像 + :return: 带有绘制边缘的原始图像和边缘掩码 + ''' + bin_img_processed = self.close_operation(bin_img, kernel_size=(15, 15)) + contours, _ = cv2.findContours(bin_img_processed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return original_img, np.zeros_like(bin_img) + max_contour = max(contours, key=cv2.contourArea) + epsilon = 0.0006 * cv2.arcLength(max_contour, True) + approx = cv2.approxPolyDP(max_contour, epsilon, True) + cv2.drawContours(original_img, [approx], -1, (0, 255, 0), 3) + mask = np.zeros_like(bin_img) + cv2.drawContours(mask, [max_contour], -1, (255), thickness=cv2.FILLED) + return original_img, mask + + def draw_tomato_edge_convex_hull(self, original_img, bin_img): + ''' + 在原始图像上绘制最大西红柿轮廓的凸包。 + :param original_img: 原始 BGR 图像 + :param bin_img: 西红柿的二值图像 + :return: 带有绘制凸包的原始图像 + ''' + bin_img_blurred = cv2.GaussianBlur(bin_img, (5, 5), 0) + contours, _ = cv2.findContours(bin_img_blurred, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return original_img + max_contour = max(contours, key=cv2.contourArea) + hull = cv2.convexHull(max_contour) + cv2.drawContours(original_img, [hull], -1, (0, 255, 0), 3) + return original_img + + + def bitwise_and_rgb_with_binary(self, rgb_img, bin_img): + ''' + 将 RGB 图像与二值图像进行按位与操作,用于将二值区域应用于原始图像。 + :param rgb_img: 原始 RGB 图像 + :param bin_img: 二值图像 + :return: 按位与后的结果图像 + ''' + bin_img_3channel = cv2.cvtColor(bin_img, cv2.COLOR_GRAY2BGR) + result = cv2.bitwise_and(rgb_img, bin_img_3channel) + return result + + def extract_max_connected_area(self, image, lower_hsv, upper_hsv): + ''' + 提取图像中满足 HSV 范围条件的最大连通区域,并填充孔洞。 + :param image: 输入的 BGR 图像 + :param lower_hsv: HSV 范围的下限 + :param upper_hsv: HSV 范围的上限 + :return: 处理后的图像 + ''' + hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) + mask = cv2.inRange(hsv, lower_hsv, upper_hsv) + num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8) + largest_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) + new_bin_img = np.zeros_like(mask) + new_bin_img[labels == largest_label] = 255 + img_filled = new_bin_img.copy() + height, width = new_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(new_bin_img, img_filled_inv) + return img_filled + +#百香果RGB处理模型 +class Passion_fruit: + def __init__(self, hue_value=setting.hue_value, hue_delta=setting.hue_delta, + value_target=setting.value_target, value_delta=setting.value_delta): + # 初始化常用参数 + self.hue_value = hue_value + self.hue_delta = hue_delta + self.value_target = value_target + self.value_delta = value_delta + + def create_mask(self, hsv_image): + # 创建H通道阈值掩码 + lower_hue = np.array([self.hue_value - self.hue_delta, 0, 0]) + upper_hue = np.array([self.hue_value + self.hue_delta, 255, 255]) + hue_mask = cv2.inRange(hsv_image, lower_hue, upper_hue) + # 创建V通道排除中心值的掩码 + lower_value_1 = np.array([0, 0, 0]) + upper_value_1 = np.array([180, 255, self.value_target - self.value_delta]) + lower_value_2 = np.array([0, 0, self.value_target + self.value_delta]) + upper_value_2 = np.array([180, 255, 255]) + value_mask_1 = cv2.inRange(hsv_image, lower_value_1, upper_value_1) + value_mask_1 = cv2.bitwise_not(value_mask_1) + value_mask_2 = cv2.inRange(hsv_image, lower_value_2, upper_value_2) + value_mask = cv2.bitwise_and(value_mask_1, value_mask_2) + + # 合并H通道和V通道掩码 + return cv2.bitwise_and(hue_mask, value_mask) + + def apply_morphology(self, mask): + # 应用形态学操作 + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) + return cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) + + def find_largest_component(self, mask): + if mask is None or mask.size == 0 or np.all(mask == 0): + logging.info("RGB 图像为空或全黑,返回一个全黑RGB图像。") + return np.zeros((setting.n_rgb_rows, setting.n_rgb_cols, setting.n_rgb_bands), dtype=np.uint8) \ + if mask is None else np.zeros_like(mask) + # 寻找最大连通组件 + num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, 4, cv2.CV_32S) + if num_labels < 2: + return None # 没有找到显著的组件 + max_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) # 跳过背景 + return (labels == max_label).astype(np.uint8) * 255 + def draw_contours_on_image(self, original_image, mask_image): + """ + 在原图上绘制轮廓 + :param original_image: 原图的NumPy数组 + :param mask_image: 轮廓mask的NumPy数组 + :return: 在原图上绘制轮廓后的图像 + """ + # 确保mask_image是二值图像 + _, binary_mask = cv2.threshold(mask_image, 127, 255, cv2.THRESH_BINARY) + # 查找mask图像中的轮廓 + contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + # 在原图上绘制轮廓 + cv2.drawContours(original_image, contours, -1, (0, 255, 0), 2) + return original_image + + def bitwise_and_rgb_with_binary(self, rgb_img, bin_img): + ''' + 将 RGB 图像与二值图像进行按位与操作,用于将二值区域应用于原始图像。 + :param rgb_img: 原始 RGB 图像 + :param bin_img: 二值图像 + :return: 按位与后的结果图像 + ''' + # 检查 RGB 图像是否为空或全黑 + if rgb_img is None or rgb_img.size == 0 or np.all(rgb_img == 0): + logging.info("RGB 图像为空或全黑,返回一个全黑RGB图像。") + return np.zeros((setting.n_rgb_rows, setting.n_rgb_cols, setting.n_rgb_bands), dtype=np.uint8) \ + if rgb_img is None else np.zeros_like(rgb_img) + # 检查二值图像是否为空或全黑 + if bin_img is None or bin_img.size == 0 or np.all(bin_img == 0): + logging.info("二值图像为空或全黑,返回一个全黑RGB图像。") + return np.zeros((setting.n_rgb_rows, setting.n_rgb_cols, setting.n_rgb_bands), dtype=np.uint8) \ + if bin_img is None else np.zeros_like(bin_img) + # 转换二值图像为三通道 + try: + bin_img_3channel = cv2.cvtColor(bin_img, cv2.COLOR_GRAY2BGR) + except cv2.error as e: + logging.error(f"转换二值图像时发生错误: {e}") + return np.zeros_like(rgb_img) + # 进行按位与操作 + try: + result = cv2.bitwise_and(rgb_img, bin_img_3channel) + except cv2.error as e: + logging.error(f"执行按位与操作时发生错误: {e}") + return np.zeros_like(rgb_img) + return result + + def extract_green_pixels_cv(self,image): + ''' + 提取图像中的绿色像素。 + :param image: + :return: + ''' + image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + # Define the HSV range for green + lower_green = np.array([setting.low_H, setting.low_S, setting.low_V]) + upper_green = np.array([setting.high_H, setting.high_S, setting.high_V]) + # Convert the image to HSV + hsv = cv2.cvtColor(image_rgb, cv2.COLOR_RGB2HSV) + # Create the mask + mask = cv2.inRange(hsv, lower_green, upper_green) + # Bitwise-AND mask and original image + res = cv2.bitwise_and(image_rgb, image_rgb, mask=mask) + # Convert result to BGR for display + res_bgr = cv2.cvtColor(res, cv2.COLOR_RGB2BGR) + return mask + + def pixel_comparison(self, defect, mask): + ''' + 比较两幅图像的像素值,如果相同则赋值为0,不同则赋值为255。 + :param defect: + :param mask: + :return: + ''' + # 确保图像是二值图像 + _, defect_binary = cv2.threshold(defect, 127, 255, cv2.THRESH_BINARY) + _, mask_binary = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) + # 执行像素比较 + green_img = np.where(defect_binary == mask_binary, 0, 255).astype(np.uint8) + return green_img + +#糖度预测模型 +class Spec_predict(object): + def __init__(self, load_from=None, debug_mode=False): + self.debug_mode = debug_mode + self.log = utils.Logger(is_to_file=debug_mode) + if load_from is not None: + self.load(load_from) + else: + self.model = RandomForestRegressor(n_estimators=100) + + def load(self, path): + if not os.path.isabs(path): + self.log.log('Path is relative, converting to absolute path.') + path = os.path.abspath(path) + + if not os.path.exists(path): + self.log.log(f'Model file not found at path: {path}') + raise FileNotFoundError(f'Model file not found at path: {path}') + + with open(path, 'rb') as f: + model_dic = joblib.load(f) + self.model = model_dic + self.log.log(f'Model loaded successfully') + + def predict(self, data_x): + ''' + 预测数据 + :param data_x: 重塑为二维数组的数据 + :return: 预测结果——糖度 + ''' + # 对数据进行切片,筛选谱段 + #qt_test进行测试时如果读取的是(30,30,224)需要解开注释进行数据切片,筛选谱段 + # data_x = data_x[ :25, :, setting.selected_bands ] + # 将筛选后的数据重塑为二维数组,每行代表一个样本 + data_x = data_x.reshape(-1, setting.n_spec_rows * setting.n_spec_cols * setting.n_spec_bands) + data_y = self.model.predict(data_x) + return data_y[0] + +#数据处理模型 +class Data_processing: + 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 + + 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): + # 检查图像是否为空或全黑 + if image_array is None or image_array.size == 0 or np.all(image_array == 0): + logging.info("输入的图像为空或全黑,返回一个全黑图像。") + return np.zeros_like(image_array) if image_array is not None else np.zeros((100, 100), dtype=np.uint8) + # 应用中值滤波 + image_filtered = cv2.medianBlur(image_array, 5) + # 形态学闭操作 + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) + image_closed = cv2.morphologyEx(image_filtered, cv2.MORPH_CLOSE, kernel) + # 查找轮廓 + contours, _ = cv2.findContours(image_closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + # 创建空白图像以绘制轮廓 + image_contours = np.zeros_like(image_array) + # 进行多边形拟合并填充轮廓 + for contour in contours: + epsilon = 0.001 * cv2.arcLength(contour, True) + approx = cv2.approxPolyDP(contour, epsilon, True) + if cv2.contourArea(approx) > 100: # 仅处理较大的轮廓 + cv2.drawContours(image_contours, [approx], -1, (255, 255, 255), -1) + return image_contours + + def analyze_ellipse(self, image_array): + # 查找白色区域的轮廓 + _, binary_image = cv2.threshold(image_array, 127, 255, cv2.THRESH_BINARY) + contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + # 初始化变量用于存储最大轮廓的长径和短径 + major_axis = 0 + minor_axis = 0 + # 对每个找到的轮廓,找出可以包围它的最小椭圆,并计算长径和短径 + for contour in contours: + if len(contour) >= 5: # 至少需要5个点来拟合椭圆 + ellipse = cv2.fitEllipse(contour) + (center, axes, orientation) = ellipse + major_axis0 = max(axes) + minor_axis0 = min(axes) + # 更新最大的长径和短径 + if major_axis0 > major_axis: + major_axis = major_axis0 + minor_axis = minor_axis0 + + return major_axis, minor_axis + + def analyze_defect(self, image): + # 确保传入的图像为单通道numpy数组 + if len(image.shape) != 2: + raise ValueError("Image must be a single-channel numpy array.") + + # 应用阈值将图像转为二值图,目标为255,背景为0 + _, binary_image = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY_INV) + + # 计算连通域 + num_labels, labels_im, stats, centroids = cv2.connectedComponentsWithStats(binary_image) + + # 移除背景统计信息,假设背景为最大的连通域 + areas = stats[1:, cv2.CC_STAT_AREA] + num_labels -= 1 + + # 过滤面积大于指定阈值的连通域 + filtered_areas = areas[areas <= self.area_threshold] + 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): + """ + 根据西红柿的长径、短径和直径估算其体积。 + 使用椭圆体积公式计算体积。 + 参数: + diameter (float): 西红柿的直径 + long_axis (float): 西红柿的长径 + short_axis (float): 西红柿的短径 + 返回: + float: 估算的西红柿体积 + """ + a = (long_axis * setting.pixel_length_ratio) / 2 + b = (short_axis * setting.pixel_length_ratio) / 2 + volume = 4 / 3 * np.pi * a * b * b + weight = round(volume * self.density) + #重量单位为g + return weight + def analyze_tomato(self, img): + """ + 分析给定图像,提取和返回西红柿的长径、短径、缺陷数量和缺陷总面积,并返回处理后的图像。 + 使用 Tomoto 类的图像处理方法,以及自定义的尺寸和缺陷信息获取函数。 + 参数: + img (numpy.ndarray): 输入的 BGR 图像 + 返回: + tuple: (长径, 短径, 缺陷区域个数, 缺陷区域总像素, 处理后的图像) + """ + tomato = Tomato() # 创建 Tomato 类的实例 + img = cv2.cvtColor(img,cv2.COLOR_RGB2BGR) + s_l = tomato.extract_s_l(img) + thresholded_s_l = tomato.threshold_segmentation(s_l, setting.threshold_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) + org_defect = tomato.bitwise_and_rgb_with_binary(edge, new_bin_img) + fore = tomato.bitwise_and_rgb_with_binary(img, mask) + fore_g_r_t = tomato.threshold_segmentation(tomato.extract_g_r(fore), threshold=setting.threshold_fore_g_r_t) + res = cv2.bitwise_or(new_bin_img, fore_g_r_t) + nogreen = tomato.bitwise_and_rgb_with_binary(edge, res) + # 统计白色像素点个数 + # print(np.sum(fore_g_r_t == 255)) + # print(np.sum(mask == 255)) + # print(np.sum(fore_g_r_t == 255) / np.sum(mask == 255)) + if np.sum(mask == 255) == 0: + green_percentage = 0 + else: + green_percentage = np.sum(fore_g_r_t == 255) / np.sum(mask == 255) + green_percentage = round(green_percentage, 2) + # 获取西红柿的尺寸信息 + long_axis, short_axis = self.analyze_ellipse(mask) + # 获取缺陷信息 + 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 格式 + rp = cv2.cvtColor(nogreen, cv2.COLOR_BGR2RGB) + #直径单位为cm + diameter = (long_axis + short_axis) * setting.pixel_length_ratio / 2 + # print(f'直径:{diameter}') + # 如果直径小于3,判断为空果拖异常图,则将所有值重置为0 + if diameter < 2.5: + diameter = 0 + green_percentage = 0 + number_defects = 0 + total_pixels = 0 + rp = cv2.cvtColor(np.ones((setting.n_rgb_rows, setting.n_rgb_cols, setting.n_rgb_bands), + dtype=np.uint8), cv2.COLOR_BGR2RGB) + return diameter, green_percentage, number_defects, total_pixels, rp + + def analyze_passion_fruit(self, img): + if img is None: + logging.error("Error: 无图像数据.") + return None + + # 创建PassionFruit类的实例 + pf = Passion_fruit() + + img = cv2.cvtColor(img,cv2.COLOR_RGB2BGR) + hsv_image = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + combined_mask = pf.create_mask(hsv_image) + combined_mask = pf.apply_morphology(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) + fore = pf.bitwise_and_rgb_with_binary(img, contour_mask) + mask = pf.extract_green_pixels_cv(fore) + green_img = pf.pixel_comparison(defect, mask) + if np.sum(contour_mask == 255) == 0: + green_percentage = 0 + else: + green_percentage = np.sum(green_img == 255) / np.sum(contour_mask == 255) + green_percentage = round(green_percentage, 2) + long_axis, short_axis = self.analyze_ellipse(contour_mask) + #重量单位为g,加上了一点随机数 + weight_real = self.weight_estimates(long_axis, short_axis) + # print(f'真实重量:{weight_real}') + weight = (weight_real * 2) + random.randint(0, 30) + # print(f'估算重量:{weight}') + if weight > 255: + weight = random.randint(30, 65) + + number_defects, total_pixels = self.analyze_defect(filled_img) + edge = pf.draw_contours_on_image(img, contour_mask) + org_defect = pf.bitwise_and_rgb_with_binary(edge, max_mask) + rp = cv2.cvtColor(org_defect, cv2.COLOR_BGR2RGB) + #直径单位为cm + diameter = (long_axis + short_axis) * setting.pixel_length_ratio / 2 + # print(f'直径:{diameter}') + if diameter < 2.5: + diameter = 0 + green_percentage = 0 + weight = 0 + number_defects = 0 + total_pixels = 0 + rp = cv2.cvtColor(np.ones((setting.n_rgb_rows, setting.n_rgb_cols, setting.n_rgb_bands), + dtype=np.uint8), cv2.COLOR_BGR2RGB) + return diameter, green_percentage, weight, number_defects, total_pixels, rp + + def process_data(seif, cmd: str, images: list, spec: any, pipe: Pipe, detector: Spec_predict) -> bool: + """ + 处理指令 + + :param cmd: 指令类型 + :param images: 图像数据列表 + :param spec: 光谱数据 + :param detector: 模型 + :return: 是否处理成功 + """ + # pipe = Pipe() + diameter_axis_list = [] + max_defect_num = 0 # 初始化最大缺陷数量为0 + max_total_defect_area = 0 # 初始化最大总像素数为0 + + for i, img in enumerate(images): + if cmd == 'TO': + # 番茄 + diameter, green_percentage, number_defects, total_pixels, rp = seif.analyze_tomato(img) + if i <= 2: + diameter_axis_list.append(diameter) + max_defect_num = max(max_defect_num, number_defects) + max_total_defect_area = max(max_total_defect_area, total_pixels) + if i == 1: + rp_result = rp + gp = round(green_percentage, 2) + + elif cmd == 'PF': + # 百香果 + diameter, green_percentage, weight, number_defects, total_pixels, rp = seif.analyze_passion_fruit(img) + if i <= 2: + diameter_axis_list.append(diameter) + max_defect_num = max(max_defect_num, number_defects) + max_total_defect_area = max(max_total_defect_area, total_pixels) + if i == 1: + rp_result = rp + weight = weight + gp = round(green_percentage, 2) + + else: + logging.error(f'错误指令,指令为{cmd}') + return False + + diameter = round(sum(diameter_axis_list) / 3, 2) + + if cmd == 'TO': + brix = 0 + weight = 0 + # print(f'预测的brix值为:{brix}; 预测的直径为:{diameter}; 预测的重量为:{weight}; 预测的绿色比例为:{gp};' + # f' 预测的缺陷数量为:{max_defect_num}; 预测的总缺陷面积为:{max_total_defect_area};') + 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) + return response + elif cmd == 'PF': + brix = detector.predict(spec) + if diameter == 0: + brix = 0 + # print(f'预测的brix值为:{brix}; 预测的直径为:{diameter}; 预测的重量为:{weight}; 预测的绿色比例为:{green_percentage};' + # f' 预测的缺陷数量为:{max_defect_num}; 预测的总缺陷面积为:{max_total_defect_area};') + response = pipe.send_data(cmd=cmd, brix=brix, green_percentage=gp, diameter=diameter, weight=weight, + defect_num=max_defect_num, total_defect_area=max_total_defect_area, rp=rp_result) + return response diff --git a/20240627Actual_deployed/config.py b/20240627Actual_deployed/config.py new file mode 100644 index 0000000..f5f2662 --- /dev/null +++ b/20240627Actual_deployed/config.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/6/17 下午3:36 +# @Author : TG +# @File : config.py +# @Software: PyCharm + +from root_dir import ROOT_DIR + +class Config: + #文件相关参数 + #预热参数 + n_spec_rows, n_spec_cols, n_spec_bands = 25, 30, 13 + n_rgb_rows, n_rgb_cols, n_rgb_bands = 613, 800, 3 + tomato_img_dir = ROOT_DIR / 'models' / 'TO.bmp' + passion_fruit_img_dir = ROOT_DIR / 'models' / 'PF.bmp' + #模型路径 + #糖度模型 + brix_model_path = ROOT_DIR / 'models' / 'passion_fruit.joblib' + #图像分类模型 + imgclassifier_model_path = ROOT_DIR / 'models' / 'imgclassifier.joblib' + imgclassifier_class_indices_path = ROOT_DIR / 'models' / 'class_indices.json' + + + #classifer.py参数 + #tomato + find_reflection_threshold = 190 + extract_g_r_factor = 1.5 + + #passion_fruit + hue_value = 37 + hue_delta = 10 + value_target = 25 + value_delta = 10 + + #提取绿色像素参数 + low_H = 0 + low_S = 100 + low_V = 0 + high_H = 60 + high_S = 180 + high_V = 60 + + #spec_predict + #筛选谱段并未使用,在qt取数据时已经筛选 + selected_bands = [8, 9, 10, 48, 49, 50, 77, 80, 103, 108, 115, 143, 145] + + #data_processing + #根据标定数据计算的参数,实际长度/像素长度,单位cm + pixel_length_ratio = 6.3/425 + #绿叶面积阈值,高于此阈值认为连通域是绿叶 + area_threshold = 20000 + #百香果密度(g/cm^3) + density = 0.652228972 + #百香果面积比例,每个像素代表的实际面积(cm^2) + area_ratio = 0.00021973702422145334 + + #def analyze_tomato + #s_l通道阈值 + threshold_s_l = 180 + threshold_fore_g_r_t = 20 + diff --git a/20240627Actual_deployed/main.py b/20240627Actual_deployed/main.py new file mode 100644 index 0000000..b6eb523 --- /dev/null +++ b/20240627Actual_deployed/main.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/4/20 18:45 +# @Author : TG +# @File : main.py +# @Software: PyCharm + +import sys +import os +from root_dir import ROOT_DIR +from classifer import Spec_predict, Data_processing +import logging +from utils import Pipe +import numpy as np +from config import Config +import time + +def main(is_debug=False): + setting = Config() + file_handler = logging.FileHandler(os.path.join(ROOT_DIR, 'tomato-passion_fruit.log'), encoding='utf-8') + file_handler.setLevel(logging.DEBUG if is_debug else logging.WARNING) + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setLevel(logging.DEBUG if is_debug else logging.WARNING) + logging.basicConfig(format='%(asctime)s %(filename)s[line:%(lineno)d] - %(levelname)s - %(message)s', + handlers=[file_handler, console_handler], + level=logging.DEBUG) + #模型加载 + detector = Spec_predict() + detector.load(path=setting.brix_model_path) + dp = Data_processing() + print('系统初始化中...') + #模型预热 + #与qt_test测试时需要注释掉预热,模型接收尺寸为(25,30,13),qt_test发送的数据为(30,30,224),需要对数据进行切片(classifer.py第379行) + _ = detector.predict(np.ones((setting.n_spec_rows, setting.n_spec_cols, setting.n_spec_bands), dtype=np.uint16)) + time.sleep(1) + print('系统初始化完成') + + rgb_receive_name = r'\\.\pipe\rgb_receive' + rgb_send_name = r'\\.\pipe\rgb_send' + spec_receive_name = r'\\.\pipe\spec_receive' + pipe = Pipe(rgb_receive_name, rgb_send_name, spec_receive_name) + rgb_receive, rgb_send, spec_receive = pipe.create_pipes(rgb_receive_name, rgb_send_name, spec_receive_name) + # 预热循环,只处理cmd为'YR'的数据 + # 当接收到的第一个指令预热命令时,结束预热循环 + while True: + data = pipe.receive_rgb_data(rgb_receive) + cmd, _ = pipe.parse_img(data) + if cmd == 'YR': + break + #主循环 + q = 1 + while True: + #RGB图像部分 + images = [] + cmd = None + for _ in range(5): + data = pipe.receive_rgb_data(rgb_receive) + cmd, img = pipe.parse_img(data) + #默认全为有果 + prediction = 1 + if prediction == 1: + images.append(img) + else: + response = pipe.send_data(cmd='KO', brix=0, diameter=0, green_percentage=0, weigth=0, defect_num=0, + total_defect_area=0, rp=np.zeros((100, 100, 3), dtype=np.uint8)) + logging.info("图像中无果,跳过此图像") + continue + + if cmd not in ['TO', 'PF', 'YR', 'KO']: + logging.error(f'错误指令,指令为{cmd}') + continue + #Spec数据部分 + spec = None + if cmd == 'PF': + spec_data = pipe.receive_spec_data(spec_receive) + _, spec = pipe.parse_spec(spec_data) + #数据处理部分 + if images: # 确保images不为空 + response = dp.process_data(cmd, images, spec, pipe, detector) + if response: + logging.info(f'处理成功,响应为: {response}') + else: + logging.error('处理失败') + else: + logging.error("没有有效的图像进行处理") + print(f'第{q}个果子处理完成') + q += 1 + + +if __name__ == '__main__': + ''' + python与qt采用windows下的命名管道进行通信,数据流按照约定的通信协议进行 + 数据处理逻辑为:连续接收5张RGB图,然后根据解析出的指令部分决定是否接收一张光谱图,然后进行处理,最后将处理得到的指标结果进行编码回传 + ''' + main(is_debug=False) diff --git a/20240627Actual_deployed/models/PF.bmp b/20240627Actual_deployed/models/PF.bmp new file mode 100644 index 0000000..44fbe47 Binary files /dev/null and b/20240627Actual_deployed/models/PF.bmp differ diff --git a/20240627Actual_deployed/models/TO.bmp b/20240627Actual_deployed/models/TO.bmp new file mode 100644 index 0000000..51d04e6 Binary files /dev/null and b/20240627Actual_deployed/models/TO.bmp differ diff --git a/20240627Actual_deployed/models/class_indices.json b/20240627Actual_deployed/models/class_indices.json new file mode 100644 index 0000000..fd39595 --- /dev/null +++ b/20240627Actual_deployed/models/class_indices.json @@ -0,0 +1,4 @@ +{ + "0": "exist", + "1": "no_exist" +} \ No newline at end of file diff --git a/20240627Actual_deployed/models/passion_fruit.joblib b/20240627Actual_deployed/models/passion_fruit.joblib new file mode 100644 index 0000000..604c96c Binary files /dev/null and b/20240627Actual_deployed/models/passion_fruit.joblib differ diff --git a/20240627Actual_deployed/qt_test.py b/20240627Actual_deployed/qt_test.py new file mode 100644 index 0000000..51e4e68 --- /dev/null +++ b/20240627Actual_deployed/qt_test.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/6/16 17:13 +# @Author : TG +# @File : qt_test.py +# @Software: PyCharm + +import sys +import os +from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QLabel, QVBoxLayout, QWidget +from PyQt5.QtGui import QPixmap, QImage +import win32file +from PIL import Image +import numpy as np +import cv2 + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Tomato Image Sender") + self.setGeometry(100, 100, 800, 600) + + central_widget = QWidget() + self.setCentralWidget(central_widget) + + layout = QVBoxLayout() + central_widget.setLayout(layout) + + self.image_label = QLabel() + layout.addWidget(self.image_label) + + self.rgb_send_name = r'\\.\pipe\rgb_receive' # 发送数据管道名对应 main.py 的接收数据管道名 + self.rgb_receive_name = r'\\.\pipe\rgb_send' # 接收数据管道名对应 main.py 的发送数据管道名 + self.spec_send_name = r'\\.\pipe\spec_receive' # 发送数据管道名对应 main.py 的接收数据管道名 + + # 连接main.py创建的命名管道 + self.rgb_send = win32file.CreateFile( + self.rgb_send_name, + win32file.GENERIC_WRITE, + 0, + None, + win32file.OPEN_EXISTING, + 0, + None + ) + + self.rgb_receive = win32file.CreateFile( + self.rgb_receive_name, + win32file.GENERIC_READ, + 0, + None, + win32file.OPEN_EXISTING, + 0, + None + ) + + self.spec_send = win32file.CreateFile( + self.spec_send_name, + win32file.GENERIC_WRITE, + 0, + None, + win32file.OPEN_EXISTING, + 0, + None + ) + + def send_image_group(self, image_dir): + ''' + 发送图像数据 + :param image_dir: bmp和raw文件所在文件夹 + :return: + ''' + rgb_files = [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith(('.bmp'))][:5] + spec_files = [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith('.raw')][:1] + + self.send_YR() + for _ in range(5): + for image_path in rgb_files: + img = cv2.imread(image_path, cv2.IMREAD_COLOR) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + img = np.asarray(img, dtype=np.uint8) + + + try: + # win32file.WriteFile(self.rgb_send, len(img_data).to_bytes(4, byteorder='big')) + height = img.shape[0] + width = img.shape[1] + height = height.to_bytes(2, byteorder='big') + width = width.to_bytes(2, byteorder='big') + img_data = img.tobytes() + length = (len(img_data) + 6).to_bytes(4, byteorder='big') + # cmd = 'TO':测试番茄数据;cmd = 'PF':测试百香果数据 + cmd = 'PF' + data_send = length + cmd.upper().encode('ascii') + height + width + img_data + win32file.WriteFile(self.rgb_send, data_send) + print(f'发送的图像数据长度: {len(data_send)}') + except Exception as e: + print(f"数据发送失败. 错误原因: {e}") + + if spec_files: + spec_file = spec_files[0] + with open(spec_file, 'rb') as f: + spec_data = f.read() + + try: + # win32file.WriteFile(self.spec_send, len(spec_data).to_bytes(4, byteorder='big')) + # print(f"发送的光谱数据长度: {len(spec_data)}") + heigth = 30 + weight = 30 + bands = 224 + heigth = heigth.to_bytes(2, byteorder='big') + weight = weight.to_bytes(2, byteorder='big') + bands = bands.to_bytes(2, byteorder='big') + length = (len(spec_data)+8).to_bytes(4, byteorder='big') + # cmd = 'TO':测试番茄数据;cmd = 'PF':测试百香果数据 + cmd = 'PF' + data_send = length + cmd.upper().encode('ascii') + heigth + weight + bands + spec_data + win32file.WriteFile(self.spec_send, data_send) + print(f'发送的光谱数据长度: {len(data_send)}') + print(f'spec长度: {len(spec_data)}') + except Exception as e: + print(f"数据发送失败. 错误原因: {e}") + + self.receive_result() + + def send_YR(self): + ''' + 发送预热指令 + :return: + ''' + length = 2 + length = length.to_bytes(4, byteorder='big') + cmd = 'YR' + data_send = length + cmd.upper().encode('ascii') + try: + win32file.WriteFile(self.rgb_send, data_send) + print("发送预热指令成功") + except Exception as e: + print(f"发送预热指令失败. 错误原因: {e}") + + def receive_result(self): + try: + # 读取结果数据 + # 读取4个字节的数据长度信息,并将其转换为整数 + data_length = int.from_bytes(win32file.ReadFile(self.rgb_receive, 4)[1], byteorder='big') + print(f"应该接收到的数据长度: {data_length}") + # 根据读取到的数据长度,读取对应长度的数据 + data = win32file.ReadFile(self.rgb_receive, data_length)[1] + print(f"实际接收到的数据长度: {len(data)}") + # 解析数据 + cmd_result = data[:2].decode('ascii').strip().upper() + brix = (int.from_bytes(data[2:4], byteorder='big')) / 1000 + green_percentage = (int.from_bytes(data[4:5], byteorder='big')) / 100 + diameter = (int.from_bytes(data[5:7], byteorder='big')) / 100 + weight = int.from_bytes(data[7:8], byteorder='big') + defect_num = int.from_bytes(data[8:10], byteorder='big') + total_defect_area = (int.from_bytes(data[10:14], byteorder='big')) / 1000 + heigth = int.from_bytes(data[14:16], byteorder='big') + width = int.from_bytes(data[16:18], byteorder='big') + rp = data[18:] + img = np.frombuffer(rp, dtype=np.uint8).reshape(heigth, width, -1) + print(f"指令:{cmd_result}, 糖度值:{brix}, 绿色占比:{green_percentage}, 直径:{diameter}cm, " + f"预估重量:{weight}g, 缺陷个数:{defect_num}, 缺陷面积:{total_defect_area}cm^2, 结果图的尺寸:{img.shape}") + + + # 显示结果图像 + image = Image.fromarray(img) + qimage = QImage(image.tobytes(), image.size[0], image.size[1], QImage.Format_RGB888) + pixmap = QPixmap.fromImage(qimage) + self.image_label.setPixmap(pixmap) + + except Exception as e: + print(f"数据接收失败. 错误原因: {e}") + + def open_file_dialog(self): + directory_dialog = QFileDialog() + directory_dialog.setFileMode(QFileDialog.Directory) + if directory_dialog.exec_(): + selected_directory = directory_dialog.selectedFiles()[0] + self.send_image_group(selected_directory) + +if __name__ == "__main__": + ''' + 1. 创建Qt应用程序 + 2. 创建主窗口 + 3. 显示主窗口 + 4. 打开文件对话框 + 5. 进入Qt事件循环 + ''' + #运行main.py后,运行qt_test.py + #运行qt_test.py后,选择文件夹,自动读取文件夹下的bmp和raw文件,发送到main.py + #main.py接收到数据后,返回结果数据,qt_test.py接收到结果数据,显示图片 + #为确保测试正确,测试文件夹下的文件数量应该为5个bmp文件和1个raw文件 + app = QApplication(sys.argv) + main_window = MainWindow() + main_window.show() + main_window.open_file_dialog() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/20240627Actual_deployed/requirements.txt b/20240627Actual_deployed/requirements.txt new file mode 100644 index 0000000..ac73d32 --- /dev/null +++ b/20240627Actual_deployed/requirements.txt @@ -0,0 +1,7 @@ +joblib==1.2.0 +numpy==1.25.0 +Pillow==9.4.0 +pywin32==305 +scikit_learn==1.2.2 +opencv-python==4.6.0.66 +scikit-learn==1.2.2 \ No newline at end of file diff --git a/20240627Actual_deployed/root_dir.py b/20240627Actual_deployed/root_dir.py new file mode 100644 index 0000000..a3d55ab --- /dev/null +++ b/20240627Actual_deployed/root_dir.py @@ -0,0 +1,4 @@ +import pathlib + +file_path = pathlib.Path(__file__) +ROOT_DIR = file_path.parent \ No newline at end of file diff --git a/20240627Actual_deployed/tomato-passion_fruit.log b/20240627Actual_deployed/tomato-passion_fruit.log new file mode 100644 index 0000000..e69de29 diff --git a/20240627Actual_deployed/utils.py b/20240627Actual_deployed/utils.py new file mode 100644 index 0000000..39f1ec8 --- /dev/null +++ b/20240627Actual_deployed/utils.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/4/20 18:24 +# @Author : TG +# @File : utils.py +# @Software: PyCharm + +import os +import win32file +import win32pipe +import time +import logging +import numpy as np +from config import Config as setting + +class Pipe: + def __init__(self, rgb_receive_name, rgb_send_name, spec_receive_name): + self.rgb_receive_name = rgb_receive_name + self.rgb_send_name = rgb_send_name + self.spec_receive_name = spec_receive_name + self.rgb_receive = None + self.rgb_send = None + self.spec_receive = None + + def create_pipes(self, rgb_receive_name, rgb_send_name, spec_receive_name): + while True: + try: + # 打开或创建命名管道 + self.rgb_receive = win32pipe.CreateNamedPipe( + rgb_receive_name, + win32pipe.PIPE_ACCESS_INBOUND, + win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_WAIT, + 1, 80000000, 80000000, 0, None + ) + self.rgb_send = win32pipe.CreateNamedPipe( + rgb_send_name, + win32pipe.PIPE_ACCESS_OUTBOUND, # 修改为输出模式 + win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_WAIT, + 1, 80000000, 80000000, 0, None + ) + self.spec_receive = win32pipe.CreateNamedPipe( + spec_receive_name, + win32pipe.PIPE_ACCESS_INBOUND, + win32pipe.PIPE_TYPE_BYTE | win32pipe.PIPE_WAIT, + 1, 200000000, 200000000, 0, None + ) + print("pipe管道创建成功,等待连接...") + time.sleep(0.5) + print('Fruit1程序启动成功,Fruit2程序正在启动中,预计需要10秒') + time.sleep(0.5) + print('请勿关闭Fruit1程序,否则Fruit2程序将无法正常运行!') + time.sleep(0.5) + print('等待中..........') + # 等待发送端连接 + win32pipe.ConnectNamedPipe(self.rgb_receive, None) + print("rgb_receive connected.") + # 等待发送端连接 + win32pipe.ConnectNamedPipe(self.rgb_send, None) + print("rgb_send connected.") + win32pipe.ConnectNamedPipe(self.spec_receive, None) + print("spec_receive connected.") + print('Fruit2程序启动成功!') + return self.rgb_receive, self.rgb_send, self.spec_receive + + except Exception as e: + print(f"管道创建连接失败,失败原因: {e}") + print("等待5秒后重试...") + time.sleep(5) + continue + + def receive_rgb_data(self, rgb_receive): + try: + # 读取图片数据长度 + len_img = win32file.ReadFile(rgb_receive, 4, None) + data_size = int.from_bytes(len_img[1], byteorder='big') + # 读取实际图片数据 + result, data = win32file.ReadFile(rgb_receive, data_size, None) + # 检查读取操作是否成功 + if result != 0: + logging.error(f"读取失败,错误代码: {result}") + return None + # 返回成功读取的数据 + return data + except Exception as e: + logging.error(f"数据接收失败,错误原因: {e}") + return None + + def receive_spec_data(self, spec_receive): + try: + # 读取光谱数据长度 + len_spec = win32file.ReadFile(spec_receive, 4, None) + data_size = int.from_bytes(len_spec[1], byteorder='big') + # 读取光谱数据 + result, spec_data = win32file.ReadFile(spec_receive, data_size, None) + # 检查读取操作是否成功 + if result != 0: + logging.error(f"读取失败,错误代码: {result}") + return None + # 返回成功读取的数据 + return spec_data + except Exception as e: + logging.error(f"数据接收失败,错误原因: {e}") + return None + + def parse_img(self, data: bytes) -> (str, any): + """ + 图像数据转换. + :param data:接收到的报文 + :return: 指令类型和内容 + """ + try: + assert len(data) > 1 + except AssertionError: + logging.error('指令转换失败,长度不足2') + return '', None + cmd, data = data[:2], data[2:] + cmd = cmd.decode('ascii').strip().upper() + # 如果收到的是预热指令'YR',直接返回命令和None,不处理图像数据 + if cmd == 'YR': + return cmd, None + n_rows, n_cols, img = data[:2], data[2:4], data[4:] + try: + n_rows, n_cols = [int.from_bytes(x, byteorder='big') for x in [n_rows, n_cols]] + except Exception as e: + logging.error(f'长宽转换失败, 错误代码{e}, 报文大小: n_rows:{n_rows}, n_cols:{n_cols}') + return '', None + try: + assert n_rows * n_cols * 3 == len(img) + # 因为是float32类型 所以长度要乘12 ,如果是uint8则乘3 + except AssertionError: + logging.error('图像指令转换失败,数据长度错误') + return '', None + img = np.frombuffer(img, dtype=np.uint8).reshape(n_rows, n_cols, -1) + return cmd, img + + def parse_spec(self, data: bytes) -> (str, any): + """ + 光谱数据转换. + :param data:接收到的报文 + :return: 指令类型和内容 + """ + try: + assert len(data) > 2 + except AssertionError: + logging.error('指令转换失败,长度不足3') + return '', None + cmd, data = data[:2], data[2:] + cmd = cmd.decode('ascii').strip().upper() + + n_rows, n_cols, n_bands, spec = data[:2], data[2:4], data[4:6], data[6:] + try: + n_rows, n_cols, n_bands = [int.from_bytes(x, byteorder='big') for x in [n_rows, n_cols, n_bands]] + except Exception as e: + logging.error(f'长宽转换失败, 错误代码{e}, 报文大小: n_rows:{n_rows}, n_cols:{n_cols}, n_bands:{n_bands}') + return '', None + try: + assert n_rows * n_cols * n_bands * 2 == len(spec) + except AssertionError: + logging.error('图像指令转换失败,数据长度错误') + return '', None + spec = np.frombuffer(spec, dtype=np.uint16).reshape((n_rows, n_bands, -1)).transpose(0, 2, 1) + return cmd, spec + + def send_data(self,cmd:str, brix, green_percentage, weight, diameter, defect_num, total_defect_area, rp): + ''' + 发送数据 + :param cmd: + :param brix: + :param green_percentage: + :param weight: + :param diameter: + :param defect_num: + :param total_defect_area: + :param rp: + :return: + ''' + cmd = cmd.strip().upper() + cmd_type = 'RE' + cmd_re = cmd_type.upper().encode('ascii') + img = np.asarray(rp, dtype=np.uint8) # 将图像转换为 NumPy 数组 + height = img.shape[0] # 获取图像的高度 + width = img.shape[1] # 获取图像的宽度 + height = height.to_bytes(2, byteorder='big') + width = width.to_bytes(2, byteorder='big') + img_bytes = img.tobytes() + diameter = int(diameter * 100).to_bytes(2, byteorder='big') + defect_num = defect_num.to_bytes(2, byteorder='big') + total_defect_area = int(total_defect_area * 1000).to_bytes(4, byteorder='big') + length = len(img_bytes) + 18 + length = length.to_bytes(4, byteorder='big') + if cmd == 'TO': + brix = 0 + brix = brix.to_bytes(2, byteorder='big') + gp = int(green_percentage * 100).to_bytes(1, byteorder='big') + weight = 0 + weight = weight.to_bytes(1, byteorder='big') + send_message = (length + cmd_re + brix + gp + diameter + weight + + defect_num + total_defect_area + height + width + img_bytes) + elif cmd == 'PF': + brix = int(brix * 1000).to_bytes(2, byteorder='big') + gp = int(green_percentage * 100).to_bytes(1, byteorder='big') + weight = weight.to_bytes(1, byteorder='big') + send_message = (length + cmd_re + brix + gp + diameter + weight + + defect_num + total_defect_area + height + width + img_bytes) + elif cmd == 'KO': + brix = 0 + brix = brix.to_bytes(2, byteorder='big') + gp = 0 + gp = gp.to_bytes(1, byteorder='big') + weight = 0 + weight = weight.to_bytes(1, byteorder='big') + defect_num = 0 + defect_num = defect_num.to_bytes(2, byteorder='big') + total_defect_area = 0 + total_defect_area = total_defect_area.to_bytes(4, byteorder='big') + height = setting.n_rgb_rows + height = height.to_bytes(2, byteorder='big') + width = setting.n_rgb_cols + width = width.to_bytes(2, byteorder='big') + img_bytes = np.zeros((setting.n_rgb_rows, setting.n_rgb_cols, setting.n_rgb_bands), + dtype=np.uint8).tobytes() + length = (18).to_bytes(4, byteorder='big') + send_message = (length + cmd_re + brix + gp + diameter + weight + + defect_num + total_defect_area + height + width + img_bytes) + try: + win32file.WriteFile(self.rgb_send, send_message) + except Exception as e: + logging.error(f'发送指令失败,错误类型:{e}') + return False + return True + +def create_file(file_name): + """ + 创建文件 + :param file_name: 文件名 + :return: None + """ + if os.path.exists(file_name): + print("文件存在:%s" % file_name) + return False + # os.remove(file_name) # 删除已有文件 + if not os.path.exists(file_name): + print("文件不存在,创建文件:%s" % file_name) + open(file_name, 'a').close() + return True + +class Logger(object): + def __init__(self, is_to_file=False, path=None): + self.is_to_file = is_to_file + if path is None: + path = "tomato-passion_fruit.log" + self.path = path + create_file(path) + + def log(self, content): + if self.is_to_file: + with open(self.path, "a") as f: + print(time.strftime("[%Y-%m-%d_%H-%M-%S]:"), file=f) + print(content, file=f) + else: + print(content)