增加模块;增加主调用命令

This commit is contained in:
2026-01-07 16:36:47 +08:00
commit 2d4b170a45
109 changed files with 55763 additions and 0 deletions

View File

@ -0,0 +1,171 @@
# coding: utf-8
# The code is written by Linghui
import numpy as np
import matplotlib.pyplot as plt
import cv2
from PIL import Image
from skimage import data
from math import floor, ceil
from skimage.feature import graycomatrix, graycoprops
def main():
pass
def image_patch(img2, slide_window, h, w):
image = img2
window_size = slide_window
patch = np.zeros((slide_window, slide_window, h, w), dtype=np.uint8)
for i in range(patch.shape[2]):
for j in range(patch.shape[3]):
patch[:, :, i, j] = img2[i : i + slide_window, j : j + slide_window]
return patch
def calcu_glcm(img, vmin=0, vmax=255, nbit=64, slide_window=5, step=[2], angle=[0]):
mi, ma = vmin, vmax
h, w = img.shape
# Compressed gray rangevmin: 0-->0, vmax: 256-1 -->nbit-1
bins = np.linspace(mi, ma+1, nbit+1)
img1 = np.digitize(img, bins) - 1
# (512, 512) --> (slide_window, slide_window, 512, 512)
img2 = cv2.copyMakeBorder(img1, floor(slide_window/2), floor(slide_window/2)
, floor(slide_window/2), floor(slide_window/2), cv2.BORDER_REPLICATE) # 图像扩充
patch = np.zeros((slide_window, slide_window, h, w), dtype=np.uint8)
patch = image_patch(img2, slide_window, h, w)
# Calculate GLCM (5, 5, 512, 512) --> (64, 64, 512, 512)
# greycomatrix(image, distances, angles, levels=None, symmetric=False, normed=False)
glcm = np.zeros((nbit, nbit, len(step), len(angle), h, w), dtype=np.float32)
for i in range(patch.shape[2]):
for j in range(patch.shape[3]):
glcm[:, :, :, :, i, j]= graycomatrix(patch[:, :, i, j], step, angle, levels=nbit).astype(np.float32)
return glcm
def calcu_glcm_mean(glcm, nbit=64):
'''
calc glcm mean
'''
mean = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
mean += glcm[i,j].astype(np.float32) * i / (nbit)**2
return mean
def calcu_glcm_variance(glcm, nbit=64):
'''
calc glcm variance
'''
mean = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
mean += glcm[i, j].astype(np.float32) * i / (nbit)**2
variance = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
variance += glcm[i, j].astype(np.float32) * (i - mean)**2
return variance
def calcu_glcm_homogeneity(glcm, nbit=64):
'''
calc glcm Homogeneity
'''
Homogeneity = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
Homogeneity += glcm[i,j].astype(np.float32) / (1.+(i-j)**2)
return Homogeneity
def calcu_glcm_contrast(glcm, nbit=64):
'''
calc glcm contrast
'''
contrast = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
contrast += glcm[i, j].astype(np.float32) * (i-j)**2
return contrast
def calcu_glcm_dissimilarity(glcm, nbit=64):
'''
calc glcm dissimilarity
'''
dissimilarity = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
dissimilarity += glcm[i, j].astype(np.float32) * np.abs(i-j)
return dissimilarity
def calcu_glcm_entropy(glcm, nbit=64):
'''
calc glcm entropy
'''
eps = 0.00001
entropy = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
entropy -= glcm[i, j].astype(np.float32) * np.log10(glcm[i, j].astype(np.float32) + eps)
return entropy
def calcu_glcm_energy(glcm, nbit=64):
'''
calc glcm energy or second moment
'''
energy = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
energy += glcm[i, j].astype(np.float32)**2
return energy
def calcu_glcm_correlation(glcm, nbit=64):
'''
calc glcm correlation (Unverified result)
'''
mean = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
mean += glcm[i, j].astype(np.float32) * i / (nbit)**2
variance = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
variance += glcm[i, j].astype(np.float32) * (i - mean)**2
correlation = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
correlation += ((i - mean) * (j - mean) * (glcm[i, j].astype(np.float32)**2))/variance
return correlation
def calcu_glcm_Auto_correlation(glcm, nbit=64):
'''
calc glcm auto correlation
'''
Auto_correlation = np.zeros((glcm.shape[2], glcm.shape[3]), dtype=np.float32)
for i in range(nbit):
for j in range(nbit):
Auto_correlation += glcm[i, j].astype(np.float32) * i * j
return Auto_correlation
if __name__ == '__main__':
main()

View File

@ -0,0 +1,358 @@
# coding: utf-8
# The code is written by Linghui
import numpy as np
from skimage import data
from matplotlib import pyplot as plt
import time
from PIL import Image
import spectral
import os
import warnings
warnings.filterwarnings('ignore')
# 导入GLCM计算模块
try:
# 尝试相对导入
from . import get_glcm
except ImportError:
try:
# 尝试绝对导入
import get_glcm
except ImportError:
# 如果都失败,尝试从当前目录导入
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
import get_glcm
# 使用get_glcm模块进行GLCM计算
class SpatialFeatureConfig:
"""
空间特征提取配置类
"""
def __init__(self):
# GLCM参数
self.nbit = 64 # gray levels
self.mi = 0 # min gray
self.ma = 255 # max gray
self.slide_window = 7 # sliding window size
# GLCM计算参数
self.step = [2] # step distances
self.angle = [0] # angles in radians
# 数据处理参数
self.band_index = 25 # 要处理的波段索引
# 文件路径
self.image_path = r"C:\Program Files\Spectronon3\_internal\examples\leaf_small.bip.hdr"
# 输出设置
self.output_dir = "output"
self.save_dat = True # 是否保存为dat文件
self.save_png = False # 是否保存为PNG文件
def validate(self):
"""验证配置参数的有效性"""
if not os.path.exists(self.image_path):
raise FileNotFoundError(f"图像文件不存在: {self.image_path}")
if self.band_index < 0:
raise ValueError("波段索引不能为负数")
if self.nbit <= 0:
raise ValueError("nbit必须大于0")
if self.slide_window <= 0 or self.slide_window % 2 == 0:
raise ValueError("slide_window必须为正奇数")
return True
def run_glcm_analysis(config):
"""
执行GLCM纹理特征分析
参数:
config: SpatialFeatureConfig对象
"""
start = time.time()
print('---------------0. Parameter Setting-----------------')
# 验证配置
try:
config.validate()
print("配置验证通过")
except Exception as e:
print(f"配置验证失败: {e}")
return
print('-------------------1. Load Data---------------------')
# 加载高光谱数据
img, h, w = load_hyperspectral_data(config.image_path, config.band_index)
# 提取纹理特征
features_dict = extract_texture_features(img, config)
print('---------------4. Display and Result----------------')
print(f'使用的波段: {config.band_index}')
print(f'GLCM参数: distances={config.step}, angles={[f"{a*180/np.pi:.0f}°" for a in config.angle]}')
print(f'图像尺寸: {h}x{w}')
print(f'处理时间: {time.time() - start:.2f}')
# 保存特征数据
if config.save_dat:
output_filename = f"spatial_features_band{config.band_index}"
output_path = os.path.join(config.output_dir, output_filename)
save_features_to_dat(features_dict, output_path, config)
print('GLCM纹理特征分析完成')
return features_dict
def load_hyperspectral_data(image_path, band_index=None):
"""
加载高光谱数据并可选地提取指定波段
参数:
- image_path: 高光谱图像文件路径
- band_index: 要提取的波段索引如果为None则返回整个图像
返回:
- img: 处理后的图像数据
- h, w: 图像尺寸
"""
print(f'正在读取高光谱图像: {image_path}')
try:
# 使用spectral库读取ENVI格式
img_obj = spectral.open_image(image_path)
hyperspectral_data = img_obj.load()
print(f'使用spectral库读取成功数据形状: {hyperspectral_data.shape}')
except Exception as e:
print(f'spectral库读取失败: {e}')
raise ValueError(f"无法读取图像文件: {image_path}")
# 检查数据维度
if len(hyperspectral_data.shape) != 3:
raise ValueError(f"高光谱数据应该是3维的当前形状: {hyperspectral_data.shape}")
h, w, bands = hyperspectral_data.shape
print(f'图像尺寸: {h}x{w}, 波段数: {bands}')
# 如果指定了波段索引,提取该波段
if band_index is not None:
if band_index < 0 or band_index >= bands:
raise ValueError(f"波段索引 {band_index} 超出范围 [0, {bands-1}]")
print(f'提取波段 {band_index}...')
img = hyperspectral_data[:, :, band_index].astype(np.float32)
# 归一化到0-255范围
img_min, img_max = img.min(), img.max()
if img_max > img_min:
img = np.uint8(255.0 * (img - img_min) / (img_max - img_min))
else:
img = np.zeros_like(img, dtype=np.uint8)
print(f'提取的波段数据范围: [{img_min:.2f}, {img_max:.2f}] -> [0, 255]')
else:
# 如果没有指定波段,返回第一个波段作为示例
print('未指定波段,使用第一个波段...')
img = hyperspectral_data[:, :, 0].astype(np.float32)
img_min, img_max = img.min(), img.max()
if img_max > img_min:
img = np.uint8(255.0 * (img - img_min) / (img_max - img_min))
else:
img = np.zeros_like(img, dtype=np.uint8)
return img, h, w
def save_features_to_dat(features_dict, output_path, config):
"""
将纹理特征保存为dat文件和对应的hdr文件
参数:
- features_dict: 包含各种纹理特征的字典
- output_path: 输出文件路径(不含扩展名)
- config: 配置对象
"""
try:
# 确保输出目录存在
output_dir = os.path.dirname(output_path)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
# 准备要保存的特征
feature_names = ['mean', 'variance', 'homogeneity', 'contrast',
'dissimilarity', 'entropy', 'energy', 'correlation', 'Auto_correlation']
# 将所有特征堆叠成多波段数据
feature_list = []
band_names = []
for feature_name in feature_names:
if feature_name in features_dict:
feature_data = features_dict[feature_name]
feature_list.append(feature_data)
band_names.append(feature_name)
if not feature_list:
print("没有找到要保存的特征数据")
return
# 堆叠所有特征为多波段数组
stacked_features = np.stack(feature_list, axis=-1)
print(f"特征数据形状: {stacked_features.shape}")
# 保存为dat文件二进制格式
dat_path = f"{output_path}.dat"
with open(dat_path, 'wb') as f:
# 将数据转换为float32格式保存
stacked_features.astype(np.float32).tofile(f)
print(f"特征数据已保存到: {dat_path}")
# 保存对应的hdr文件
hdr_path = f"{output_path}.hdr"
_save_hdr_file(hdr_path, stacked_features.shape, config, band_names)
print(f"头文件已保存到: {hdr_path}")
except Exception as e:
print(f"保存dat文件时出错: {e}")
def _save_hdr_file(hdr_path, data_shape, config, band_names):
"""
保存ENVI头文件
参数:
- hdr_path: 头文件路径
- data_shape: 数据形状 (height, width, bands)
- config: 配置对象
- band_names: 波段名称列表
"""
height, width, bands = data_shape
# 计算数据范围(为每个波段计算)
data_range_info = f"Data ranges vary per band (normalized 0-1)"
header_content = f"""ENVI
description = {{Spatial Texture Features [Band: {config.band_index}, Window: {config.slide_window}, Step: {config.step}, Angles: {[f"{a*180/np.pi:.0f}°" for a in config.angle]}]}}
samples = {width}
lines = {height}
bands = {bands}
header offset = 0
file type = ENVI Standard
data type = 4
interleave = bip
byte order = 0
band names = {{{', '.join(band_names)}}}
data range = {{{data_range_info}}}
GLCM_params = {{nbit: {config.nbit}, window: {config.slide_window}, step: {config.step}, angles: {config.angle}}}
"""
with open(hdr_path, 'w', encoding='utf-8') as f:
f.write(header_content)
def extract_texture_features(img, config):
"""
提取纹理特征
参数:
- img: 输入图像
- config: 配置对象
返回:
- features_dict: 包含各种纹理特征的字典
"""
print('------------------2. 计算GLCM特征---------------------')
img = img.squeeze()
# 使用get_glcm计算GLCM特征
glcm = get_glcm.calcu_glcm(img, config.mi, config.ma, config.nbit,
config.slide_window, config.step, config.angle)
print('GLCM计算完成')
print('-----------------3. 提取纹理特征-------------------')
# 提取纹理特征
# 使用最后一个step和angle的特征
i, j = len(config.step)-1, len(config.angle)-1
glcm_cut = glcm[:, :, i, j, :, :]
features_dict = {
'image_shape': img.shape,
'mean': get_glcm.calcu_glcm_mean(glcm_cut, config.nbit),
'variance': get_glcm.calcu_glcm_variance(glcm_cut, config.nbit),
'homogeneity': get_glcm.calcu_glcm_homogeneity(glcm_cut, config.nbit),
'contrast': get_glcm.calcu_glcm_contrast(glcm_cut, config.nbit),
'dissimilarity': get_glcm.calcu_glcm_dissimilarity(glcm_cut, config.nbit),
'entropy': get_glcm.calcu_glcm_entropy(glcm_cut, config.nbit),
'energy': get_glcm.calcu_glcm_energy(glcm_cut, config.nbit),
'correlation': get_glcm.calcu_glcm_correlation(glcm_cut, config.nbit),
'Auto_correlation': get_glcm.calcu_glcm_Auto_correlation(glcm_cut, config.nbit)
}
return features_dict
def main():
"""主函数:执行空间特征提取流程"""
start = time.time()
print('---------------0. Parameter Setting-----------------')
# 创建配置对象
config = SpatialFeatureConfig()
# 可以在这里修改配置参数
# config.band_index = 30 # 修改波段索引
# config.step = [2, 4, 8] # 修改步长
# config.angle = [0, np.pi/4, np.pi/2, np.pi*3/4] # 修改角度
# 验证配置
try:
config.validate()
print("配置验证通过")
except Exception as e:
print(f"配置验证失败: {e}")
return
print('-------------------1. Load Data---------------------')
# 加载高光谱数据
img, h, w = load_hyperspectral_data(config.image_path, config.band_index)
# 提取纹理特征
features_dict = extract_texture_features(img, config)
print('---------------4. Display and Result----------------')
print(f'使用的波段: {config.band_index}')
print(f'GLCM参数: distances={config.step}, angles={[f"{a*180/np.pi:.0f}°" for a in config.angle]}')
print(f'图像尺寸: {h}x{w}')
print(f'处理时间: {time.time() - start:.2f}')
# 保存特征数据
if config.save_dat:
output_filename = f"spatial_features_band{config.band_index}"
output_path = os.path.join(config.output_dir, output_filename)
save_features_to_dat(features_dict, output_path, config)
print('处理完成!')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,215 @@
# coding: utf-8
# The code is written by Linghui
import numpy as np
from skimage import data
from matplotlib import pyplot as plt
import get_glcm
import time
from PIL import Image
import spectral
import os
import warnings
warnings.filterwarnings('ignore')
# 使用get_glcm模块进行GLCM计算
def main():
pass
def load_hyperspectral_data(image_path, band_index=None):
"""
加载高光谱数据并可选地提取指定波段
参数:
- image_path: 高光谱图像文件路径
- band_index: 要提取的波段索引如果为None则返回整个图像
返回:
- img: 处理后的图像数据
- h, w: 图像尺寸
"""
print(f'正在读取高光谱图像: {image_path}')
try:
# 使用spectral库读取ENVI格式
img_obj = spectral.open_image(image_path)
hyperspectral_data = img_obj.load()
print(f'使用spectral库读取成功数据形状: {hyperspectral_data.shape}')
except Exception as e:
print(f'spectral库读取失败: {e}')
raise ValueError(f"无法读取图像文件: {image_path}")
# 检查数据维度
if len(hyperspectral_data.shape) != 3:
raise ValueError(f"高光谱数据应该是3维的当前形状: {hyperspectral_data.shape}")
h, w, bands = hyperspectral_data.shape
print(f'图像尺寸: {h}x{w}, 波段数: {bands}')
# 如果指定了波段索引,提取该波段
if band_index is not None:
if band_index < 0 or band_index >= bands:
raise ValueError(f"波段索引 {band_index} 超出范围 [0, {bands-1}]")
print(f'提取波段 {band_index}...')
img = hyperspectral_data[:, :, band_index].astype(np.float32)
# 归一化到0-255范围
img_min, img_max = img.min(), img.max()
if img_max > img_min:
img = np.uint8(255.0 * (img - img_min) / (img_max - img_min))
else:
img = np.zeros_like(img, dtype=np.uint8)
print(f'提取的波段数据范围: [{img_min:.2f}, {img_max:.2f}] -> [0, 255]')
else:
# 如果没有指定波段,返回第一个波段作为示例
print('未指定波段,使用第一个波段...')
img = hyperspectral_data[:, :, 0].astype(np.float32)
img_min, img_max = img.min(), img.max()
if img_max > img_min:
img = np.uint8(255.0 * (img - img_min) / (img_max - img_min))
else:
img = np.zeros_like(img, dtype=np.uint8)
return img, h, w
if __name__ == '__main__':
main()
start = time.time()
print('---------------0. Parameter Setting-----------------')
nbit = 64 # gray levels
mi, ma = 0, 255 # max gray and min gray
slide_window = 7 # sliding window
# step = [2, 4, 8, 16] # step
# angle = [0, np.pi/4, np.pi/2, np.pi*3/4] # angle or direction
step = [2]
angle = [0]
# 指定波段索引(可以修改)
band_index = 25 # 要处理的波段索引从0开始
print('-------------------1. Load Data---------------------')
# 修改为高光谱图像路径
image_path = r"C:\Program Files\Spectronon3\_internal\examples\leaf_small.bip.hdr"
# image_path = r"./test.tif" # 备用的TIFF图像路径
# 检查文件是否存在
if not os.path.exists(image_path):
print(f"警告: 指定的图像文件不存在: {image_path}")
print("请确保文件路径正确,或使用其他可用的图像文件")
exit(1)
# 加载高光谱数据
img, h, w = load_hyperspectral_data(image_path, band_index)
print('------------------2. 计算GLCM特征---------------------')
img = img.squeeze()
# 使用get_glcm计算GLCM特征
glcm = get_glcm.calcu_glcm(img, mi, ma, nbit, slide_window, step, angle)
print('GLCM计算完成')
print('-----------------3. 提取纹理特征-------------------')
# 提取纹理特征用于显示
# 只计算最后一个step和angle的特征用于显示
i, j = len(step)-1, len(angle)-1 # 使用最后一个配置
glcm_cut = glcm[:, :, i, j, :, :]
mean = get_glcm.calcu_glcm_mean(glcm_cut, nbit)
variance = get_glcm.calcu_glcm_variance(glcm_cut, nbit)
homogeneity = get_glcm.calcu_glcm_homogeneity(glcm_cut, nbit)
contrast = get_glcm.calcu_glcm_contrast(glcm_cut, nbit)
dissimilarity = get_glcm.calcu_glcm_dissimilarity(glcm_cut, nbit)
entropy = get_glcm.calcu_glcm_entropy(glcm_cut, nbit)
energy = get_glcm.calcu_glcm_energy(glcm_cut, nbit)
correlation = get_glcm.calcu_glcm_correlation(glcm_cut, nbit)
Auto_correlation = get_glcm.calcu_glcm_Auto_correlation(glcm_cut, nbit)
print('---------------4. Display and Result----------------')
print(f'使用的波段: {band_index}')
print(f'GLCM参数: distances={step}, angles={[f"{a*180/np.pi:.0f}°" for a in angle]}')
plt.figure(figsize=(10, 4.5))
font = {'family' : 'Times New Roman',
'weight' : 'normal',
'size' : 12,
}
plt.subplot(2,5,1)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(img, cmap ='gray')
plt.title(f'Band {band_index}', font)
plt.subplot(2,5,2)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(mean, cmap ='gray')
plt.title('Mean', font)
plt.subplot(2,5,3)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(variance, cmap ='gray')
plt.title('Variance', font)
plt.subplot(2,5,4)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(homogeneity, cmap ='gray')
plt.title('Homogeneity', font)
plt.subplot(2,5,5)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(contrast, cmap ='gray')
plt.title('Contrast', font)
plt.subplot(2,5,6)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(dissimilarity, cmap ='gray')
plt.title('Dissimilarity', font)
plt.subplot(2,5,7)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(entropy, cmap ='gray')
plt.title('Entropy', font)
plt.subplot(2,5,8)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(energy, cmap ='gray')
plt.title('Energy', font)
plt.subplot(2,5,9)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(correlation, cmap ='gray')
plt.title('Correlation', font)
plt.subplot(2,5,10)
plt.tick_params(labelbottom=False, labelleft=False)
plt.axis('off')
plt.imshow(Auto_correlation, cmap ='gray')
plt.title('Auto Correlation', font)
plt.tight_layout(pad=0.5)
# 创建输出文件名,包含波段信息
output_filename = f'GLCM_Features_Band{band_index}.png'
plt.savefig(output_filename
, format='png'
, bbox_inches = 'tight'
, pad_inches = 0
, dpi=300)
print(f'结果图像已保存到: {output_filename}')
end = time.time()
print('Code run time:', end - start)

View File

@ -0,0 +1,841 @@
# coding: utf-8
# Shape Feature Analysis Module
# 用于分析二值化图像的形状特征
import numpy as np
import cv2
import pandas as pd
from skimage import measure, morphology
from skimage.segmentation import watershed
from skimage.feature import peak_local_max
from scipy import ndimage
import spectral
import os
import warnings
from shapely.geometry import shape
import geopandas as gpd
import json
warnings.filterwarnings('ignore')
class ShapeFeatureConfig:
"""
形状特征分析配置类
"""
def __init__(self):
# 输入数据设置
self.input_type = 'dat' # 'dat' 或 'shp'
self.dat_file_path = None # dat文件路径
self.hdr_file_path = None # 对应的hdr文件路径
self.shp_file_path = None # shp文件路径
self.band_index = 0 # 要处理的波段索引对于dat文件
# 连通域处理参数
self.connectivity = 8 # 连通性4或8
self.min_area = 10 # 最小区域面积阈值
self.use_watershed = False # 是否使用分水岭算法分离相邻物体
# 分水岭算法参数
self.watershed_min_distance = 10 # 局部最大值的最小距离
# 输出设置
self.output_dir = "output"
self.output_csv = True # 是否输出CSV文件
self.save_labeled_image = False # 是否保存标记后的图像
def validate(self):
"""验证配置参数的有效性"""
if self.input_type not in ['dat', 'shp']:
raise ValueError("input_type必须是'dat''shp'")
if self.input_type == 'dat':
if not self.dat_file_path or not os.path.exists(self.dat_file_path):
raise FileNotFoundError(f"dat文件不存在: {self.dat_file_path}")
if self.hdr_file_path and not os.path.exists(self.hdr_file_path):
raise FileNotFoundError(f"hdr文件不存在: {self.hdr_file_path}")
elif self.input_type == 'shp':
if not self.shp_file_path or not os.path.exists(self.shp_file_path):
raise FileNotFoundError(f"shp文件不存在: {self.shp_file_path}")
if self.connectivity not in [4, 8]:
raise ValueError("connectivity必须是4或8")
if self.min_area < 1:
raise ValueError("min_area必须大于等于1")
return True
def load_binary_image_from_dat(dat_path, hdr_path=None, band_index=0):
"""
从dat文件加载二值化图像数据
参数:
- dat_path: dat文件路径
- hdr_path: 对应的hdr文件路径可选
- band_index: 要读取的波段索引
返回:
- binary_image: 二值化图像数组
- metadata: 元数据字典
"""
try:
# 如果提供了hdr文件路径使用spectral库读取ENVI格式
if hdr_path and os.path.exists(hdr_path):
print(f"使用hdr文件: {hdr_path}")
img_obj = spectral.open_image(hdr_path)
image_data = img_obj.load()
print(f"通过spectral库读取成功形状: {image_data.shape}")
# 如果没有hdr文件或读取失败直接读取dat文件
else:
print(f"直接读取dat文件: {dat_path}")
# 从dat文件路径推断hdr文件路径
if not hdr_path:
hdr_path = dat_path.replace('.dat', '.hdr')
if os.path.exists(hdr_path):
# 读取hdr文件获取元数据
with open(hdr_path, 'r') as f:
hdr_content = f.read()
# 解析hdr文件
lines = hdr_content.split('\n')
metadata = {}
for line in lines:
line = line.strip()
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
if key == 'samples':
metadata['samples'] = int(value)
elif key == 'lines':
metadata['lines'] = int(value)
elif key == 'bands':
metadata['bands'] = int(value)
elif key == 'data type':
metadata['data_type'] = int(value)
print(f"从hdr文件解析的元数据: {metadata}")
# 读取dat文件
with open(dat_path, 'rb') as f:
if metadata['data_type'] == 1: # 8-bit unsigned integer
image_data = np.frombuffer(f.read(), dtype=np.uint8)
else:
raise ValueError(f"不支持的数据类型: {metadata['data_type']}")
# 重塑为正确的形状 (lines, samples, bands)
image_data = image_data.reshape(metadata['lines'], metadata['samples'], metadata['bands'])
print(f"重塑后的数据形状: {image_data.shape}")
else:
# 如果没有hdr文件使用spectral库尝试读取
try:
img_obj = spectral.open_image(dat_path)
image_data = img_obj.load()
print(f"通过spectral库读取成功形状: {image_data.shape}")
except:
raise FileNotFoundError(f"找不到对应的hdr文件: {hdr_path}")
# 处理波段选择
if len(image_data.shape) == 3:
if band_index >= image_data.shape[2]:
raise ValueError(f"波段索引 {band_index} 超出范围 [0, {image_data.shape[2]-1}]")
binary_image = image_data[:, :, band_index]
else:
binary_image = image_data
# 确保是二值图像
binary_image = binary_image.astype(np.uint8)
# 对于分割结果,通常已经是二值的,但确保值范围正确
unique_values = np.unique(binary_image)
print(f"图像中的唯一值: {unique_values}")
# 如果图像值范围不是0-1转换为二值假设非零值是前景
if binary_image.max() > 1:
binary_image = (binary_image > 0).astype(np.uint8)
print("已转换为二值图像")
metadata = {
'shape': binary_image.shape,
'dtype': binary_image.dtype,
'source': 'dat',
'band_index': band_index,
'unique_values': unique_values.tolist()
}
print(f"最终图像形状: {binary_image.shape}, 数据类型: {binary_image.dtype}")
return binary_image, metadata
except Exception as e:
print(f"读取dat文件失败: {e}")
import traceback
traceback.print_exc()
raise
def load_regions_from_shp(shp_path, image_shape=None):
"""
从shp文件加载区域信息并转换为二值图像
参数:
- shp_path: shp文件路径
- image_shape: 目标图像形状 (height, width),用于创建二值图像
返回:
- binary_image: 二值化图像数组
- metadata: 元数据字典
"""
try:
# 读取shp文件
gdf = gpd.read_file(shp_path)
print(f"成功读取shp文件包含 {len(gdf)} 个几何对象")
if image_shape is None:
# 如果没有指定图像形状尝试从bounds推断
bounds = gdf.total_bounds # [minx, miny, maxx, maxy]
# 这里需要根据实际坐标系进行转换,暂时使用简单的假设
height = int(bounds[3] - bounds[1])
width = int(bounds[2] - bounds[0])
if height <= 0 or width <= 0:
raise ValueError("无法从shp文件推断图像尺寸请提供image_shape参数")
image_shape = (height, width)
# 创建二值图像
binary_image = np.zeros(image_shape, dtype=np.uint8)
# 将每个几何对象转换为像素坐标并填充
for idx, geom in enumerate(gdf.geometry):
if geom is not None:
# 这里需要坐标转换逻辑,暂时使用简化的实现
# 实际实现需要根据shp文件的坐标系进行准确转换
try:
# 简化的几何转换(实际使用时需要更复杂的坐标变换)
coords = np.array(geom.exterior.coords) if hasattr(geom, 'exterior') else np.array(geom.coords)
# 归一化坐标到图像尺寸(这只是示例,实际需要正确的地理坐标转换)
coords_normalized = coords.copy()
if len(coords) > 0:
# 创建标签图像,每个区域用不同的值标记
label_value = idx + 1
# 这里应该实现正确的几何到像素的转换
# 暂时用占位符实现
pass
except Exception as e:
print(f"处理几何对象 {idx} 时出错: {e}")
continue
# 如果没有有效的几何转换,创建一个示例图像
if binary_image.max() == 0:
print("警告未能从shp文件转换为有效的二值图像使用示例数据")
binary_image[100:200, 100:200] = 1 # 示例矩形区域
metadata = {
'shape': binary_image.shape,
'dtype': binary_image.dtype,
'source': 'shp',
'num_features': len(gdf)
}
return binary_image, metadata
except Exception as e:
print(f"读取shp文件失败: {e}")
raise
def apply_watershed_segmentation(binary_image, config):
"""
使用分水岭算法分离相邻物体
参数:
- binary_image: 二值化图像
- config: 配置对象
返回:
- labeled_image: 分水岭分割后的标记图像
- num_objects: 物体数量
"""
try:
# 计算距离变换
distance = ndimage.distance_transform_edt(binary_image)
# 找到局部最大值作为种子点
local_maxi = peak_local_max(distance,
indices=False,
footprint=np.ones((3, 3)),
labels=binary_image,
min_distance=config.watershed_min_distance)
# 标记种子点
markers = ndimage.label(local_maxi)[0]
# 应用分水岭算法
labels = watershed(-distance, markers, mask=binary_image)
# 重新标记以确保连续的标签
unique_labels = np.unique(labels)
unique_labels = unique_labels[unique_labels > 0] # 排除背景
relabeled_image = np.zeros_like(labels)
for new_label, old_label in enumerate(unique_labels, 1):
relabeled_image[labels == old_label] = new_label
num_objects = len(unique_labels)
print(f"分水岭分割完成,识别出 {num_objects} 个物体")
return relabeled_image.astype(np.int32), num_objects
except Exception as e:
print(f"分水岭分割失败: {e}")
raise
def label_connected_components(binary_image, config):
"""
标记连通域,可选择使用分水岭算法
参数:
- binary_image: 二值化图像
- config: 配置对象
返回:
- labeled_image: 标记后的图像
- num_objects: 物体数量
"""
try:
if config.use_watershed:
# 使用分水岭算法
labeled_image, num_objects = apply_watershed_segmentation(binary_image, config)
else:
# 使用标准连通域标记
if config.connectivity == 8:
structure = np.ones((3, 3), dtype=int)
else: # connectivity == 4
structure = np.array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]], dtype=int)
labeled_image, num_objects = ndimage.label(binary_image, structure=structure)
# 过滤小区域
if config.min_area > 1:
component_sizes = np.bincount(labeled_image.ravel())
too_small = component_sizes < config.min_area
too_small_mask = too_small[labeled_image]
labeled_image[too_small_mask] = 0
# 重新标记以保持连续性
unique_labels = np.unique(labeled_image)
unique_labels = unique_labels[unique_labels > 0]
relabeled_image = np.zeros_like(labeled_image)
for new_label, old_label in enumerate(unique_labels, 1):
relabeled_image[labeled_image == old_label] = new_label
labeled_image = relabeled_image
num_objects = len(unique_labels)
print(f"连通域标记完成,找到 {num_objects} 个有效区域")
return labeled_image, num_objects
except Exception as e:
print(f"连通域标记失败: {e}")
raise
def calculate_shape_features(labeled_image, config):
"""
计算所有形状特征指标
参数:
- labeled_image: 标记后的图像
- config: 配置对象
返回:
- features_df: 包含所有特征的DataFrame
- contour_coords_dict: 轮廓坐标字典
"""
try:
# 获取区域属性
regions = measure.regionprops(labeled_image)
if len(regions) == 0:
print("警告:没有找到有效的区域")
return pd.DataFrame(), {}
features_list = []
contour_coords_dict = {}
for region in regions:
try:
# 基本属性
label = region.label
area = region.area
perimeter = region.perimeter
# 边界框和最小外接矩形
min_row, min_col, max_row, max_col = region.bbox
bbox_width = max_col - min_col
bbox_height = max_row - min_row
bbox_area = bbox_width * bbox_height
# 获取轮廓坐标
coords = region.coords # 像素坐标
# 计算各种形状特征
feature_dict = {
'label': label,
'area': area,
'perimeter': perimeter,
'bbox_width': bbox_width,
'bbox_height': bbox_height,
'bbox_area': bbox_area
}
# 1. 圆形度 (Circularity) = (4 * π * 面积) / (周长²)
if perimeter > 0:
circularity = (4 * np.pi * area) / (perimeter ** 2)
else:
circularity = 0
feature_dict['circularity'] = circularity
# 2. 矩形度 (Rectangularity) = 面积 / 外接矩形面积
rectangularity = area / bbox_area if bbox_area > 0 else 0
feature_dict['rectangularity'] = rectangularity
# 3. 长宽比 (Aspect Ratio) = 宽度 / 高度
aspect_ratio = bbox_width / bbox_height if bbox_height > 0 else 0
feature_dict['aspect_ratio'] = aspect_ratio
# 4. 紧密度 (Compactness) = (周长²) / 面积
compactness = (perimeter ** 2) / area if area > 0 else 0
feature_dict['compactness'] = compactness
# 5. 偏心率 (Eccentricity) - 使用regionprops的eccentricity
eccentricity = region.eccentricity
feature_dict['eccentricity'] = eccentricity
# 6. 形状矩 (Shape Moments) - Hu矩
if hasattr(region, 'moments_hu'):
hu_moments = region.moments_hu
for i, hu_moment in enumerate(hu_moments):
feature_dict[f'hu_moment_{i+1}'] = hu_moment
else:
# 如果没有Hu矩计算为0
for i in range(7):
feature_dict[f'hu_moment_{i+1}'] = 0
# 7. 凸性 (Convexity) = 面积 / 凸包面积
convex_area = region.convex_area
convexity = area / convex_area if convex_area > 0 else 0
feature_dict['convexity'] = convexity
# 8. 固体度 (Solidity) = 面积 / 凸包面积 (与凸性相同)
solidity = convexity # 在scikit-image中solidity就是这个定义
feature_dict['solidity'] = solidity
# 9. 轮廓矩 (Contour Moments) - 使用中心矩
if hasattr(region, 'moments_central'):
central_moments = region.moments_central
feature_dict['central_moment_02'] = central_moments[0, 2] # μ02
feature_dict['central_moment_20'] = central_moments[2, 0] # μ20
feature_dict['central_moment_11'] = central_moments[1, 1] # μ11
else:
feature_dict['central_moment_02'] = 0
feature_dict['central_moment_20'] = 0
feature_dict['central_moment_11'] = 0
# 10. 质心坐标
centroid_row, centroid_col = region.centroid
feature_dict['centroid_row'] = centroid_row
feature_dict['centroid_col'] = centroid_col
# 11. 主要轴长度和角度
if hasattr(region, 'major_axis_length'):
feature_dict['major_axis_length'] = region.major_axis_length
feature_dict['minor_axis_length'] = region.minor_axis_length
feature_dict['orientation'] = region.orientation
else:
feature_dict['major_axis_length'] = 0
feature_dict['minor_axis_length'] = 0
feature_dict['orientation'] = 0
# 12. 边界坐标序列将保存到单独的JSON文件中
coords_list = coords.tolist()
coord_key = f"region_{region.label}_coords"
feature_dict['contour_coords'] = coord_key # 引用标识符
# 将轮廓坐标添加到字典中
contour_coords_dict[coord_key] = coords_list
# 13. 其他有用的特征
feature_dict['equivalent_diameter'] = region.equivalent_diameter
feature_dict['extent'] = region.extent # 面积与边界框面积之比
features_list.append(feature_dict)
except Exception as e:
print(f"计算区域 {region.label} 的特征时出错: {e}")
continue
# 创建DataFrame
features_df = pd.DataFrame(features_list)
print(f"成功计算 {len(features_df)} 个区域的形状特征")
return features_df, contour_coords_dict
except Exception as e:
print(f"计算形状特征失败: {e}")
raise
def save_features_to_csv(features_df, output_path, config, contour_coords_dict=None):
"""
将形状特征保存为CSV文件
参数:
- features_df: 特征DataFrame
- output_path: 输出文件路径(不含扩展名)
- config: 配置对象
- contour_coords_dict: 轮廓坐标字典(可选)
"""
try:
# 确保输出目录存在
output_dir = os.path.dirname(output_path)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
# 如果有轮廓坐标数据保存到单独的JSON文件
if contour_coords_dict is not None and len(contour_coords_dict) > 0:
coords_json_path = f"{output_path}_contour_coords.json"
with open(coords_json_path, 'w', encoding='utf-8') as f:
json.dump(contour_coords_dict, f, indent=2, ensure_ascii=False)
print(f"轮廓坐标已保存到: {coords_json_path}")
# 保存为CSV文件
csv_path = f"{output_path}.csv"
features_df.to_csv(csv_path, index=False, encoding='utf-8')
print(f"形状特征已保存到: {csv_path}")
print(f"共保存 {len(features_df)} 个区域的特征数据")
# 打印特征统计信息
if len(features_df) > 0:
numeric_columns = features_df.select_dtypes(include=[np.number]).columns
print(f"数值特征列数: {len(numeric_columns)}")
print(f"特征列: {list(features_df.columns)}")
return csv_path
except Exception as e:
print(f"保存CSV文件失败: {e}")
raise
def save_labeled_image(labeled_image, output_path, config):
"""
保存标记后的图像(可选)
参数:
- labeled_image: 标记后的图像
- output_path: 输出文件路径(不含扩展名)
- config: 配置对象
"""
try:
# 保存为PNG图像用于可视化
png_path = f"{output_path}_labeled.png"
# 归一化标签图像以便可视化
if labeled_image.max() > 0:
normalized_image = (labeled_image / labeled_image.max() * 255).astype(np.uint8)
else:
normalized_image = labeled_image.astype(np.uint8)
# 创建彩色标签图像
colored_image = cv2.applyColorMap(normalized_image, cv2.COLORMAP_JET)
# 保存图像
cv2.imwrite(png_path, colored_image)
print(f"标记图像已保存到: {png_path}")
# 同时保存为dat文件ENVI格式
dat_path = f"{output_path}_labeled.dat"
with open(dat_path, 'wb') as f:
labeled_image.astype(np.int32).tofile(f)
# 保存对应的hdr文件
hdr_path = f"{output_path}_labeled.hdr"
header_content = f"""ENVI
description = {{Labeled Image from Shape Analysis [Watershed: {config.use_watershed}]}}
samples = {labeled_image.shape[1]}
lines = {labeled_image.shape[0]}
bands = 1
header offset = 0
file type = ENVI Standard
data type = 3
interleave = bsq
byte order = 0
band names = {{Labeled_Result}}
classes = {labeled_image.max() + 1}
class names = {{Background{' , '.join([f'Region_{i}' for i in range(1, labeled_image.max() + 1)])}}}
"""
with open(hdr_path, 'w', encoding='utf-8') as f:
f.write(header_content)
print(f"ENVI格式标记数据已保存到: {dat_path}")
except Exception as e:
print(f"保存标记图像失败: {e}")
def analyze_shape_features(config):
"""
执行形状特征分析的主函数
参数:
- config: 配置对象
返回:
- features_df: 特征DataFrame
- labeled_image: 标记后的图像
"""
try:
print("=" * 60)
print("开始形状特征分析")
print("=" * 60)
# 1. 加载数据
print("1. 加载输入数据...")
if config.input_type == 'dat':
binary_image, metadata = load_binary_image_from_dat(
config.dat_file_path, config.hdr_file_path, config.band_index
)
elif config.input_type == 'shp':
binary_image, metadata = load_regions_from_shp(
config.shp_file_path, image_shape=None
)
else:
raise ValueError(f"不支持的输入类型: {config.input_type}")
print(f"数据加载完成,图像形状: {binary_image.shape}")
binary_image = binary_image.squeeze()
# 2. 连通域标记
print("2. 执行连通域标记...")
labeled_image, num_objects = label_connected_components(binary_image, config)
print(f"找到 {num_objects} 个连通域")
if num_objects == 0:
print("警告:没有找到有效的连通域")
return pd.DataFrame(), labeled_image
# 3. 计算形状特征
print("3. 计算形状特征...")
features_df, contour_coords_dict = calculate_shape_features(labeled_image, config)
if len(features_df) == 0:
print("警告:未能计算出有效的形状特征")
return features_df, labeled_image
# 4. 保存结果
print("4. 保存分析结果...")
# 生成输出文件名
timestamp = pd.Timestamp.now().strftime("%Y%m%d_%H%M%S")
base_name = f"shape_features_{config.input_type}"
if config.input_type == 'dat':
base_name += f"_band{config.band_index}"
if config.use_watershed:
base_name += "_watershed"
base_name += f"_{timestamp}"
output_path = os.path.join(config.output_dir, base_name)
# 保存CSV文件
if config.output_csv:
csv_path = save_features_to_csv(features_df, output_path, config, contour_coords_dict)
# 保存标记图像
if config.save_labeled_image:
save_labeled_image(labeled_image, output_path, config)
print("=" * 60)
print("形状特征分析完成!")
print(f"分析了 {len(features_df)} 个区域")
print(f"计算了 {len(features_df.columns)} 个特征指标")
if config.output_csv:
print(f"结果已保存到: {csv_path}")
print("=" * 60)
return features_df, labeled_image
except Exception as e:
print(f"形状特征分析失败: {e}")
raise
def main():
"""主函数:命令行接口"""
import argparse
parser = argparse.ArgumentParser(
description='形状特征分析工具 - 从分割结果提取形状特征',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
使用示例:
1. 从dat文件分析形状特征:
python shape_feature.py input.dat -t dat -b 0 -o shape_features
2. 从shp文件分析形状特征:
python shape_feature.py input.shp -t shp -o shape_features
3. 启用分水岭算法分离物体:
python shape_feature.py input.dat -t dat --use-watershed --watershed-min-distance 15 -o features
4. 自定义连通性和面积阈值:
python shape_feature.py input.dat -t dat -c 4 --min-area 100 --save-labeled-image -o results
输入格式:
- dat: ENVI格式的分割结果文件 (.dat + .hdr)
- shp: Shapefile格式的矢量数据
形状特征包括:
- 面积、周长、紧密度
- 圆度、矩形度、伸长度
- 质心坐标、边界框
- 方向角、主轴长度
- Hu矩不变量等几何特征
"""
)
parser.add_argument('input_path', help='输入文件路径 (.dat分割结果 或 .shp矢量文件)')
parser.add_argument('-t', '--input-type', choices=['dat', 'shp'], default='dat',
help='输入文件类型 (默认: dat)')
parser.add_argument('-o', '--output', required=True,
help='输出文件路径前缀')
# dat文件参数
parser.add_argument('-b', '--band-index', type=int, default=0,
help='dat文件的波段索引 (默认: 0)')
parser.add_argument('--hdr-path',
help='dat文件对应的hdr文件路径 (默认自动查找)')
# 连通域参数
parser.add_argument('-c', '--connectivity', type=int, choices=[4, 8], default=8,
help='连通性4或8 (默认: 8)')
parser.add_argument('--min-area', type=int, default=10,
help='最小区域面积阈值 (默认: 10)')
# 分水岭参数
parser.add_argument('--use-watershed', action='store_true',
help='启用分水岭算法分离相邻物体')
parser.add_argument('--watershed-min-distance', type=int, default=10,
help='分水岭局部最大值的最小距离 (默认: 10)')
# 输出选项
parser.add_argument('--output-dir', default='output',
help='输出目录 (默认: output)')
parser.add_argument('--save-labeled-image', action='store_true',
help='保存标记后的图像文件')
args = parser.parse_args()
try:
print("=" * 70)
print("形状特征分析工具")
print("=" * 70)
print(f"输入文件: {args.input_path}")
print(f"输入类型: {args.input_type}")
print(f"输出路径: {args.output}")
print(f"输出目录: {args.output_dir}")
if args.input_type == 'dat':
print(f"波段索引: {args.band_index}")
print(f"HDR文件: {args.hdr_path or '自动查找'}")
print(f"连通性: {args.connectivity}")
print(f"最小面积: {args.min_area}")
print(f"使用分水岭: {args.use_watershed}")
if args.use_watershed:
print(f"分水岭最小距离: {args.watershed_min_distance}")
print()
# 创建配置
config = ShapeFeatureConfig()
config.input_type = args.input_type
config.output_dir = args.output_dir
config.connectivity = args.connectivity
config.min_area = args.min_area
config.use_watershed = args.use_watershed
config.watershed_min_distance = args.watershed_min_distance
config.save_labeled_image = args.save_labeled_image
# 设置输入文件路径
if args.input_type == 'dat':
config.dat_file_path = args.input_path
config.hdr_file_path = args.hdr_path
config.band_index = args.band_index
elif args.input_type == 'shp':
config.shp_file_path = args.input_path
# 验证配置
try:
config.validate()
print("配置验证通过")
except Exception as e:
print(f"✗ 配置验证失败: {e}")
return 1
# 执行形状特征分析
print("\n开始形状特征分析...")
features_df, labeled_image = analyze_shape_features(config)
# 显示结果统计
if len(features_df) > 0:
print("\n✓ 分析完成!")
print(f"检测到的物体数量: {len(features_df)}")
print(f"提取的特征数量: {len(features_df.columns)}")
# 显示特征名称
feature_names = [col for col in features_df.columns if col not in ['label', 'centroid_row', 'centroid_col']]
print(f"形状特征: {', '.join(feature_names[:10])}{'...' if len(feature_names) > 10 else ''}")
# 显示统计信息
print("\n特征统计:")
numeric_cols = features_df.select_dtypes(include=[np.number]).columns
if len(numeric_cols) > 0:
stats = features_df[numeric_cols].describe()
print(f" 面积范围: {features_df['area'].min():.0f} - {features_df['area'].max():.0f} 像素")
print(f" 平均面积: {features_df['area'].mean():.1f} 像素")
print(f" 周长范围: {features_df['perimeter'].min():.1f} - {features_df['perimeter'].max():.1f}")
print(f" 圆度范围: {features_df['circularity'].min():.3f} - {features_df['circularity'].max():.3f}")
else:
print("\n⚠ 未检测到任何物体,请检查输入数据或调整参数")
print("\n" + "=" * 70)
print("形状特征分析完成!")
print("=" * 70)
except Exception as e:
print(f"✗ 处理失败: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == '__main__':
main()