初始提交
This commit is contained in:
391
shape_spectral.py
Normal file
391
shape_spectral.py
Normal file
@ -0,0 +1,391 @@
|
||||
# 去除过曝光像素
|
||||
# 导入必要的库
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use('TkAgg')
|
||||
from plantcv import plantcv as pcv
|
||||
import get_glcm
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
from plantcv.plantcv._helpers import _iterate_analysis, _cv2_findcontours, _object_composition, _grayscale_to_rgb, \
|
||||
_scale_size
|
||||
from plantcv.plantcv import outputs, within_frame
|
||||
from plantcv.plantcv import params
|
||||
from plantcv.plantcv._debug import _debug
|
||||
from outputs2dataframe import process_plantcv_outputs
|
||||
|
||||
|
||||
def size(img, labeled_mask, n_labels=1, label=None):
|
||||
"""A function that analyzes the shape and size of objects and outputs data.
|
||||
|
||||
Inputs:
|
||||
img = RGB or grayscale image data for plotting
|
||||
labeled_mask = Labeled mask of objects (32-bit).
|
||||
n_labels = Total number expected individual objects (default = 1).
|
||||
label = Optional label parameter, modifies the variable name of
|
||||
observations recorded (default = pcv.params.sample_label).
|
||||
|
||||
Returns:
|
||||
analysis_image = Diagnostic image showing measurements.
|
||||
|
||||
:param img: numpy.ndarray
|
||||
:param labeled_mask: numpy.ndarray
|
||||
:param n_labels: int
|
||||
:param label: str
|
||||
:return analysis_image: numpy.ndarray
|
||||
"""
|
||||
# Set lable to params.sample_label if None
|
||||
if label is None:
|
||||
label = params.sample_label
|
||||
|
||||
img = _iterate_analysis(img=img, labeled_mask=labeled_mask, n_labels=n_labels, label=label, function=_analyze_size)
|
||||
# Debugging
|
||||
_debug(visual=img, filename=os.path.join(params.debug_outdir, str(params.device) + '_shapes.png'))
|
||||
return img
|
||||
|
||||
|
||||
def _analyze_size(img, mask, label):
|
||||
"""Analyze the size and shape of individual objects."""
|
||||
|
||||
params.device += 1
|
||||
# Initialize analysis output values
|
||||
area = 0
|
||||
hull_area = 0
|
||||
solidity = 0
|
||||
perimeter = 0
|
||||
width = 0
|
||||
height = 0
|
||||
caliper_length = 0
|
||||
longest_path = 0
|
||||
cmx = 0
|
||||
cmy = 0
|
||||
hull_vertices = 0
|
||||
circularity = 0
|
||||
shape_factor = 0
|
||||
aspect_ratio = 0
|
||||
moments = {}
|
||||
|
||||
# Check if the object is touching image boundaries (QC)
|
||||
in_bounds = within_frame(mask=mask, label=label)
|
||||
|
||||
# Convert grayscale images to color
|
||||
img = _grayscale_to_rgb(img)
|
||||
# Plot image
|
||||
plt_img = np.copy(img)
|
||||
|
||||
# Find contours
|
||||
cnt, cnt_str = _cv2_findcontours(bin_img=mask)
|
||||
|
||||
# Consolidate contours
|
||||
obj = _object_composition(contours=cnt, hierarchy=cnt_str)
|
||||
contour_list = obj.squeeze().tolist()
|
||||
# Analyze shape properties if the object is large enough
|
||||
if len(obj) > 5:
|
||||
# Convex Hull
|
||||
hull = cv2.convexHull(obj)
|
||||
# Number of convex hull vertices
|
||||
hull_vertices = len(hull)
|
||||
# Convex Hull area
|
||||
hull_area = cv2.contourArea(hull)
|
||||
# Moments
|
||||
m = cv2.moments(mask, binaryImage=True)
|
||||
# Area
|
||||
area = m['m00']
|
||||
# Solidity
|
||||
solidity = area / hull_area if hull_area != 0 else 1
|
||||
# Perimeter
|
||||
perimeter = cv2.arcLength(obj, closed=True)
|
||||
# Bounding rectangle
|
||||
x, y, width, height = cv2.boundingRect(obj)
|
||||
# Centroid/Center of Mass
|
||||
cmx = m['m10'] / m['m00']
|
||||
cmy = m['m01'] / m['m00']
|
||||
# Aspect ratio
|
||||
aspect_ratio = width / float(height) if height != 0 else 0
|
||||
# Circularity (4π * area / perimeter^2)
|
||||
if perimeter > 0:
|
||||
circularity = (4 * np.pi * area) / (perimeter ** 2)
|
||||
# Shape Factor (perimeter^2 / (4π * area))
|
||||
if area > 0:
|
||||
shape_factor = (perimeter ** 2) / (4 * np.pi * area)
|
||||
|
||||
# Hu Moments (seven invariant moments)
|
||||
# hu_moments = cv2.HuMoments(m).flatten()
|
||||
|
||||
# Store Hu Moments in the moments dictionary
|
||||
# for i, hu_moment in enumerate(hu_moments):
|
||||
# moments[f"hu_moment_{i + 1}"] = hu_moment
|
||||
|
||||
# Store outputs
|
||||
outputs.add_metadata(term="image_height", datatype=int, value=np.shape(img)[0])
|
||||
outputs.add_metadata(term="image_width", datatype=int, value=np.shape(img)[1])
|
||||
outputs.add_observation(sample=label, variable='area', trait='area',
|
||||
method='plantcv.plantcv.analyze.size', scale=params.unit, datatype=int,
|
||||
value=_scale_size(area, "area"), label=params.unit)
|
||||
outputs.add_observation(sample=label, variable='convex_hull_area', trait='convex hull area',
|
||||
method='plantcv.plantcv.analyze.size', scale=params.unit, datatype=int,
|
||||
value=_scale_size(hull_area, "area"), label=params.unit)
|
||||
outputs.add_observation(sample=label, variable='solidity', trait='solidity',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=float,
|
||||
value=solidity, label='none')
|
||||
outputs.add_observation(sample=label, variable='perimeter', trait='perimeter',
|
||||
method='plantcv.plantcv.analyze.size', scale=params.unit, datatype=int,
|
||||
value=_scale_size(perimeter), label=params.unit)
|
||||
outputs.add_observation(sample=label, variable='width', trait='width',
|
||||
method='plantcv.plantcv.analyze.size', scale=params.unit, datatype=int,
|
||||
value=_scale_size(width), label=params.unit)
|
||||
outputs.add_observation(sample=label, variable='height', trait='height',
|
||||
method='plantcv.plantcv.analyze.size', scale=params.unit, datatype=int,
|
||||
value=_scale_size(height), label=params.unit)
|
||||
outputs.add_observation(sample=label, variable='longest_path', trait='longest path',
|
||||
method='plantcv.plantcv.analyze.size', scale=params.unit, datatype=int,
|
||||
value=_scale_size(float(longest_path)), label=params.unit)
|
||||
outputs.add_observation(sample=label, variable='center_of_mass', trait='center of mass',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=tuple,
|
||||
value=(cmx, cmy), label=("x", "y"))
|
||||
outputs.add_observation(sample=label, variable='convex_hull_vertices', trait='convex hull vertices',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=int,
|
||||
value=hull_vertices, label='none')
|
||||
outputs.add_observation(sample=label, variable='object_in_frame', trait='object in frame',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=bool,
|
||||
value=in_bounds, label='none')
|
||||
|
||||
# Add new features: Circularity, Shape Factor, Aspect Ratio, and Hu Moments
|
||||
outputs.add_observation(sample=label, variable='circularity', trait='circularity',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=float,
|
||||
value=circularity, label='none')
|
||||
outputs.add_observation(sample=label, variable='shape_factor', trait='shape_factor',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=float,
|
||||
value=shape_factor, label='none')
|
||||
outputs.add_observation(sample=label, variable='aspect_ratio', trait='aspect_ratio',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=float,
|
||||
value=aspect_ratio, label='none')
|
||||
|
||||
# Store Hu Moments from moments dictionary
|
||||
# for moment_name, moment_value in moments.items():
|
||||
# outputs.add_observation(sample=label, variable=moment_name, trait=moment_name,
|
||||
# method='plantcv.plantcv.analyze.size', scale='none', datatype=float,
|
||||
# value=moment_value, label='none')
|
||||
# Store the contour object in outputs
|
||||
outputs.add_observation(sample=label, variable='contour', trait='contour',
|
||||
method='plantcv.plantcv.analyze.size', scale='none', datatype=list,
|
||||
value=contour_list, label='none')
|
||||
return plt_img
|
||||
|
||||
|
||||
def _longest_axis(height, width, hull, cmx, cmy):
|
||||
"""
|
||||
Calculate the line through center of mass and point on the convex hull that is furthest away
|
||||
|
||||
:param height: int
|
||||
:param width: int
|
||||
:param hull: numpy.ndarray
|
||||
:param cmx: int
|
||||
:param cmy: int
|
||||
:return caliper_length: int
|
||||
"""
|
||||
background = np.zeros((height, width, 3), np.uint8)
|
||||
background1 = np.zeros((height, width), np.uint8)
|
||||
background2 = np.zeros((height, width), np.uint8)
|
||||
# Longest Axis: line through center of mass and point on the convex hull that is furthest away
|
||||
cv2.circle(background, (int(cmx), int(cmy)), 4, (255, 255, 255), -1)
|
||||
center_p = cv2.cvtColor(background, cv2.COLOR_BGR2GRAY)
|
||||
_, centerp_binary = cv2.threshold(center_p, 0, 255, cv2.THRESH_BINARY)
|
||||
centerpoint, _ = _cv2_findcontours(bin_img=centerp_binary)
|
||||
|
||||
dist = []
|
||||
vhull = np.vstack(hull)
|
||||
|
||||
for i, c in enumerate(vhull):
|
||||
xy = tuple(int(ci) for ci in c)
|
||||
pptest = cv2.pointPolygonTest(centerpoint[0], xy, measureDist=True)
|
||||
dist.append(pptest)
|
||||
|
||||
abs_dist = np.absolute(dist)
|
||||
max_i = np.argmax(abs_dist)
|
||||
|
||||
caliper_max_x, caliper_max_y = list(tuple(vhull[max_i]))
|
||||
caliper_mid_x, caliper_mid_y = [int(cmx), int(cmy)]
|
||||
|
||||
xdiff = float(caliper_max_x - caliper_mid_x)
|
||||
ydiff = float(caliper_max_y - caliper_mid_y)
|
||||
|
||||
# Set default values
|
||||
slope = 1
|
||||
|
||||
if xdiff != 0:
|
||||
slope = float(ydiff / xdiff)
|
||||
b_line = caliper_mid_y - (slope * caliper_mid_x)
|
||||
|
||||
if slope != 0:
|
||||
xintercept = int(-b_line / slope)
|
||||
xintercept1 = int((height - b_line) / slope)
|
||||
if 0 <= xintercept <= width and 0 <= xintercept1 <= width:
|
||||
cv2.line(background1, (xintercept1, height), (xintercept, 0), (255), params.line_thickness)
|
||||
elif xintercept < 0 or xintercept > width or xintercept1 < 0 or xintercept1 > width:
|
||||
yintercept = int(b_line)
|
||||
yintercept1 = int((slope * width) + b_line)
|
||||
cv2.line(background1, (0, yintercept), (width, yintercept1), (255), 5)
|
||||
else:
|
||||
cv2.line(background1, (width, caliper_mid_y), (0, caliper_mid_y), (255), params.line_thickness)
|
||||
|
||||
_, line_binary = cv2.threshold(background1, 0, 255, cv2.THRESH_BINARY)
|
||||
|
||||
cv2.drawContours(background2, [hull], -1, (255), -1)
|
||||
_, hullp_binary = cv2.threshold(background2, 0, 255, cv2.THRESH_BINARY)
|
||||
|
||||
caliper = cv2.multiply(line_binary, hullp_binary)
|
||||
|
||||
caliper_y, caliper_x = np.array(caliper.nonzero())
|
||||
caliper_matrix = np.vstack((caliper_x, caliper_y))
|
||||
caliper_transpose = np.transpose(caliper_matrix)
|
||||
caliper_length = len(caliper_transpose)
|
||||
|
||||
caliper_transpose1 = np.lexsort((caliper_y, caliper_x))
|
||||
caliper_transpose2 = [(caliper_x[i], caliper_y[i]) for i in caliper_transpose1]
|
||||
caliper_transpose = np.array(caliper_transpose2)
|
||||
|
||||
return caliper_length, caliper_transpose
|
||||
|
||||
|
||||
def process_image_glcm(img, nbit=64, mi=0, ma=255, slide_window=3, step=[2], angle=[0]):
|
||||
# 如果只有单波段图像,调整为多波段格式
|
||||
if len(img.shape) == 2:
|
||||
img = np.expand_dims(img, axis=0)
|
||||
|
||||
bands, h, w = img.shape
|
||||
print(f"Image has {bands} bands.")
|
||||
|
||||
# 初始化存储所有波段特征的列表
|
||||
all_band_features = []
|
||||
|
||||
for b in range(bands):
|
||||
print(f"Processing band {b + 1}...")
|
||||
|
||||
# 获取当前波段并归一化到 0-255
|
||||
band = img[b]
|
||||
band = np.uint8(255.0 * (band - np.min(band)) / (np.max(band) - np.min(band)))
|
||||
|
||||
# 计算 GLCM
|
||||
glcm = get_glcm.calcu_glcm(band, mi, ma, nbit, slide_window, step, angle)
|
||||
|
||||
# 计算当前波段的 GLCM 特征
|
||||
features = []
|
||||
for i in range(glcm.shape[2]):
|
||||
for j in range(glcm.shape[3]):
|
||||
glcm_cut = glcm[:, :, i, j, :, :]
|
||||
features.append(get_glcm.calcu_glcm_mean(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_variance(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_homogeneity(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_contrast(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_dissimilarity(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_entropy(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_energy(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_correlation(glcm_cut, nbit))
|
||||
features.append(get_glcm.calcu_glcm_Auto_correlation(glcm_cut, nbit))
|
||||
|
||||
# 堆叠当前波段的所有特征
|
||||
features = np.array(features)
|
||||
all_band_features.append(features)
|
||||
|
||||
# 将所有波段的特征堆叠
|
||||
all_band_features = np.vstack(all_band_features)
|
||||
|
||||
return all_band_features
|
||||
|
||||
|
||||
def process_images(full_bil_path, mask, outdir='None', debug="None"):
|
||||
"""
|
||||
处理光谱图像和掩膜文件,进行分析并保存结果。
|
||||
|
||||
Parameters:
|
||||
full_bil_path (str): 光谱图像文件路径,包含 .bil 文件。
|
||||
mask (numpy.ndarray): 16位标签掩膜图像,每个对象的像素值对应其标签ID。
|
||||
outdir (str): 输出结果文件夹路径。
|
||||
debug (str): 调试模式('plot' 或 'print',默认为 'print')。
|
||||
|
||||
Returns:
|
||||
combined_data: 分析结果的数据列表
|
||||
"""
|
||||
|
||||
# 定义选项类
|
||||
class options:
|
||||
def __init__(self):
|
||||
self.image = full_bil_path # 输入的光谱图像路径
|
||||
self.debug = debug # 设置调试模式:'plot' 或 'print'
|
||||
self.writeimg = False # 是否保存图片
|
||||
self.result = outdir # 输出结果文件路径
|
||||
self.outdir = outdir # 输出文件夹路径
|
||||
|
||||
# 初始化参数
|
||||
args = options()
|
||||
|
||||
# 设置 PlantCV 的全局参数
|
||||
pcv.params.debug = args.debug
|
||||
pcv.params.dpi = 100 # 设置 DPI
|
||||
pcv.params.text_size = 1 # 文本大小
|
||||
pcv.params.text_thickness = 1 # 文本粗细
|
||||
|
||||
# 读取光谱图像(ENVI 格式)
|
||||
spectral_array = pcv.readimage(filename=args.image, mode='envi')
|
||||
#过曝区域掩膜
|
||||
bath_100 = spectral_array.array_data[:, :, 100]
|
||||
bath_over = pcv.threshold.binary(gray_img=bath_100, threshold=11000, object_type='dark')
|
||||
|
||||
# mask已经是16位标签掩膜,直接使用
|
||||
# 确保掩膜是正确的数据类型
|
||||
if mask.dtype != np.uint16:
|
||||
mask = mask.astype(np.uint16)
|
||||
|
||||
# 获取标签数量和标签值
|
||||
unique_labels = np.unique(mask)
|
||||
# 移除背景标签(通常是0)
|
||||
unique_labels = unique_labels[unique_labels != 0]
|
||||
num = len(unique_labels)
|
||||
|
||||
print(f"Number of objects in mask: {num}")
|
||||
|
||||
# 转换过曝掩膜为二值格式(0和1)
|
||||
bath_over_binary = (bath_over > 0).astype(np.uint16)
|
||||
|
||||
# 创建用于形状分析的标签掩膜(去除过曝区域的版本)
|
||||
# labeled_mask = mask * bath_over_binary
|
||||
labeled_mask = mask
|
||||
|
||||
# 获取剩余的有效标签
|
||||
valid_labels = np.unique(labeled_mask)
|
||||
valid_labels = valid_labels[valid_labels != 0]
|
||||
num_valid = len(valid_labels)
|
||||
|
||||
print(f"Number of objects after removing overexposed regions: {num_valid}")
|
||||
|
||||
# 分析每个对象的形状和大小
|
||||
# 使用二值图像用于可视化(将标签掩膜转换为显示格式)
|
||||
binary_img = (mask > 0).astype(np.uint8) * 255
|
||||
shape_img = size(img=binary_img, labeled_mask=labeled_mask, n_labels=num_valid)
|
||||
|
||||
# 分析光谱反射率
|
||||
# 应用过曝掩膜
|
||||
labeled_spectral_mask = mask * bath_over_binary
|
||||
|
||||
spectral_hist = pcv.analyze.spectral_reflectance(
|
||||
hsi=spectral_array,
|
||||
labeled_mask=labeled_spectral_mask,
|
||||
n_labels=num_valid,
|
||||
label=None
|
||||
)
|
||||
|
||||
observations = pcv.outputs.observations
|
||||
# 将结果转换为列表
|
||||
combined_data = process_plantcv_outputs(observations)
|
||||
|
||||
return combined_data
|
||||
|
||||
# # 示例:批量处理指定路径下的光谱图像和掩膜文件
|
||||
# bil_path = r'D:\WQ\test\Traindata-05'
|
||||
# mask_path = r'D:\WQ\test\mask'
|
||||
# outdir = r"D:\WQ\test" # 输出文件夹路径
|
||||
#
|
||||
# process_images(bil_path, mask_path, outdir)
|
||||
Reference in New Issue
Block a user