| import json
|
| import os
|
| import sys
|
| from PIL import Image
|
| import numpy as np
|
| import cv2
|
| from osgeo import gdal, ogr, osr
|
|
|
|
|
| Image.MAX_IMAGE_PIXELS = None
|
|
|
|
|
|
|
|
|
|
|
| def create_geotiff_from_png(png_path, ref_tiff_path, out_tiff_path):
|
| """
|
| 将拼接好的二值PNG图转换为具有地理参考信息的GeoTIFF。
|
| 使用GDAL直接读取PNG,避免内存问题。
|
| """
|
| print(f"\n--- 🌐 步骤 II: 转换为 GeoTIFF ---")
|
|
|
|
|
| ds_ref = gdal.Open(ref_tiff_path, gdal.GA_ReadOnly)
|
| if ds_ref is None:
|
| raise RuntimeError(f"致命错误: 无法打开原始参考文件 {ref_tiff_path}")
|
|
|
| geo_transform = ds_ref.GetGeoTransform()
|
| projection = ds_ref.GetProjection()
|
|
|
|
|
| print("⏳ 正在读取PNG文件...")
|
| try:
|
|
|
| ds_png = gdal.Open(png_path, gdal.GA_ReadOnly)
|
| if ds_png is None:
|
| raise RuntimeError(f"无法使用GDAL打开PNG文件: {png_path}")
|
|
|
|
|
| png_width = ds_png.RasterXSize
|
| png_height = ds_png.RasterYSize
|
|
|
| print(f"✅ PNG尺寸: {png_width} x {png_height}")
|
|
|
|
|
| png_band = ds_png.GetRasterBand(1)
|
|
|
| except Exception as e:
|
|
|
| print(f"⚠️ GDAL无法打开PNG,尝试使用PIL分块读取...")
|
| try:
|
| img_png = Image.open(png_path)
|
| png_width, png_height = img_png.size
|
|
|
|
|
| chunk_size = 1000
|
| data_chunks = []
|
| for y_start in range(0, png_height, chunk_size):
|
| y_end = min(y_start + chunk_size, png_height)
|
| box = (0, y_start, png_width, y_end)
|
| chunk = np.array(img_png.crop(box).convert('L'))
|
| data_chunks.append(chunk)
|
| if (y_start // chunk_size + 1) % 10 == 0:
|
| print(f" 已读取 {y_end}/{png_height} 行...")
|
|
|
|
|
| data = np.vstack(data_chunks)
|
| img_png.close()
|
|
|
| except Exception as e2:
|
| raise RuntimeError(f"读取PNG图像失败: {e2}")
|
|
|
|
|
| driver = gdal.GetDriverByName("GTiff")
|
| if os.path.exists(out_tiff_path):
|
| driver.Delete(out_tiff_path)
|
|
|
| print(f"⏳ 正在创建GeoTIFF文件...")
|
| ds_new = driver.Create(
|
| out_tiff_path,
|
| png_width,
|
| png_height,
|
| 1,
|
| gdal.GDT_Byte,
|
| options=['COMPRESS=DEFLATE', 'NUM_THREADS=ALL_CPUS', 'TILED=YES', 'BLOCKXSIZE=256', 'BLOCKYSIZE=256']
|
| )
|
|
|
|
|
| ds_new.SetGeoTransform(geo_transform)
|
| ds_new.SetProjection(projection)
|
|
|
| band = ds_new.GetRasterBand(1)
|
|
|
|
|
| if 'ds_png' in locals():
|
| print("⏳ 正在分块写入数据...")
|
| block_size = 1000
|
| for y_start in range(0, png_height, block_size):
|
| y_end = min(y_start + block_size, png_height)
|
| data_chunk = png_band.ReadAsArray(0, y_start, png_width, y_end - y_start)
|
| band.WriteArray(data_chunk, 0, y_start)
|
| if (y_start // block_size + 1) % 10 == 0:
|
| print(f" 已写入 {y_end}/{png_height} 行...")
|
| ds_png = None
|
| else:
|
|
|
| print("⏳ 正在写入数据...")
|
| band.WriteArray(data)
|
|
|
| band.SetNoDataValue(0)
|
|
|
|
|
| ds_new = None
|
| ds_ref = None
|
| if 'data' in locals():
|
| del data
|
|
|
| print(f"✅ GeoTIFF 转换成功,保存至: {out_tiff_path}")
|
|
|
|
|
| add_transparent_color(out_tiff_path)
|
|
|
| return out_tiff_path
|
|
|
| def add_transparent_color(raster_path):
|
| """把 0 值设成完全透明,1 值设成任意可见色 (适用于 GDT_Byte 或 GDT_UInt16)"""
|
| try:
|
| ds = gdal.Open(raster_path, gdal.GA_Update)
|
| if ds is None:
|
| print(f"警告: 无法以更新模式打开 {raster_path} 进行颜色表设置。")
|
| return
|
|
|
| band = ds.GetRasterBand(1)
|
|
|
| if band.DataType != gdal.GDT_Byte:
|
|
|
| print(f"警告: 栅格类型 {gdal.GetDataTypeName(band.DataType)} 可能不支持颜色表。")
|
|
|
| ct = gdal.ColorTable()
|
| ct.SetColorEntry(0, (0, 0, 0, 0))
|
| ct.SetColorEntry(1, (255, 0, 0, 180))
|
| band.SetColorTable(ct)
|
| band.SetRasterColorInterpretation(gdal.GCI_PaletteIndex)
|
| ds = None
|
| print(f"✅ {os.path.basename(raster_path)} 已添加透明颜色表。")
|
| except Exception as e:
|
| print(f"❌ 颜色表设置失败: {e}")
|
|
|
| def raster2polygon(in_raster_path, out_shp_path,
|
| field_name='DN',
|
| connected_8=True,
|
| sieve_size=0,
|
| target_value=255,
|
| contour_approx_epsilon=None,
|
| smooth_gaussian=True,
|
| gaussian_kernel_size=5,
|
| use_spline=False,
|
| spline_points=100,
|
| min_area=0.0,
|
| check_topology=True):
|
| """
|
| 使用OpenCV轮廓检测和轮廓近似进行矢量化,获得平滑边界(减少锐角)
|
|
|
| Args:
|
| in_raster_path: 输入栅格路径
|
| out_shp_path: 输出矢量路径
|
| field_name: 字段名
|
| connected_8: 是否使用8连通(保留参数以兼容旧代码,但新方法使用OpenCV轮廓检测)
|
| sieve_size: 碎斑过滤阈值(像素数)
|
| target_value: 目标像素值
|
| contour_approx_epsilon: 轮廓近似精度(像素),None表示自动计算
|
| 值越小越精确但顶点越多,值越大越平滑但可能丢失细节
|
| 建议值:0.5-2.0像素(减小以获得更平滑的边界)
|
| smooth_gaussian: 是否在矢量化前对栅格进行高斯模糊平滑(默认True)
|
| gaussian_kernel_size: 高斯模糊核大小(奇数,建议3-7,默认5)
|
| use_spline: 是否使用样条插值进一步平滑边界(默认False,会增加计算时间)
|
| spline_points: 样条插值的点数(仅在use_spline=True时有效)
|
| min_area: 最小面积阈值(平方米),小于此值的多边形将被过滤(默认0.0不过滤)
|
| check_topology: 是否进行拓扑检查和修复(默认True)
|
| """
|
| print(f"\n--- 🗺️ 步骤 III: 栅格转矢量(轮廓平滑方法)---")
|
|
|
| ds = gdal.Open(in_raster_path, gdal.GA_ReadOnly)
|
| if ds is None:
|
| raise RuntimeError(f'无法打开输入栅格:{in_raster_path}')
|
|
|
| band = ds.GetRasterBand(1)
|
| data = band.ReadAsArray()
|
|
|
|
|
| geo_transform = ds.GetGeoTransform()
|
| projection = ds.GetProjection()
|
|
|
|
|
| target_pixels = np.sum(data == target_value)
|
| total_pixels = data.size
|
| print(f"目标像素(值={target_value}): {target_pixels:,} / {total_pixels:,}")
|
|
|
| if target_pixels == 0:
|
| print("⚠️ 没有找到目标像素,跳过矢量化")
|
| ds = None
|
| return None
|
|
|
|
|
| mask_data = np.where(data == target_value, 255, 0).astype(np.uint8)
|
|
|
|
|
| if sieve_size > 0:
|
| print(f"⏳ 碎斑过滤 (阈值: {sieve_size} 像素)...")
|
|
|
| kernel_size = max(3, sieve_size // 2)
|
| if kernel_size % 2 == 0:
|
| kernel_size += 1
|
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
|
| mask_data = cv2.morphologyEx(mask_data, cv2.MORPH_OPEN, kernel)
|
|
|
|
|
| if smooth_gaussian:
|
| print(f"⏳ 高斯模糊平滑 (核大小: {gaussian_kernel_size})...")
|
|
|
| if gaussian_kernel_size % 2 == 0:
|
| gaussian_kernel_size += 1
|
| mask_data = cv2.GaussianBlur(mask_data, (gaussian_kernel_size, gaussian_kernel_size), 0)
|
|
|
| _, mask_data = cv2.threshold(mask_data, 127, 255, cv2.THRESH_BINARY)
|
|
|
|
|
| out_dir = os.path.dirname(out_shp_path)
|
| if out_dir and not os.path.exists(out_dir):
|
| os.makedirs(out_dir, exist_ok=True)
|
|
|
|
|
| if os.path.exists(out_shp_path):
|
| for ext in ['.shp', '.shx', '.dbf', '.prj', '.qpj']:
|
| try:
|
| os.remove(out_shp_path.replace('.shp', ext))
|
| except:
|
| pass
|
|
|
|
|
| driver = ogr.GetDriverByName('ESRI Shapefile')
|
| out_ds = driver.CreateDataSource(out_shp_path)
|
|
|
| srs = osr.SpatialReference()
|
| srs.ImportFromWkt(projection)
|
|
|
| out_layer = out_ds.CreateLayer(
|
| name=os.path.splitext(os.path.basename(out_shp_path))[0],
|
| geom_type=ogr.wkbPolygon,
|
| srs=srs
|
| )
|
|
|
| fd = ogr.FieldDefn(field_name, ogr.OFTInteger)
|
| out_layer.CreateField(fd)
|
|
|
|
|
| print("⏳ 正在检测轮廓...")
|
|
|
| contours, hierarchy = cv2.findContours(
|
| mask_data,
|
| cv2.RETR_CCOMP,
|
| cv2.CHAIN_APPROX_NONE
|
| )
|
|
|
| print(f" 检测到 {len(contours)} 个轮廓")
|
|
|
|
|
| if contour_approx_epsilon is None:
|
|
|
|
|
| pixel_size_x = abs(geo_transform[1])
|
| pixel_size_y = abs(geo_transform[5])
|
| avg_pixel_size = (pixel_size_x + pixel_size_y) / 2
|
|
|
|
|
|
|
|
|
| if avg_pixel_size < 0.001:
|
| contour_approx_epsilon = 1.0
|
| else:
|
| contour_approx_epsilon = max(0.5, avg_pixel_size / 16.0)
|
|
|
| print(f" 轮廓近似精度: {contour_approx_epsilon:.2f} 像素(值越小边界越平滑)")
|
|
|
|
|
| def smooth_contour_with_spline(contour, num_points=100):
|
| """
|
| 使用样条插值平滑轮廓,减少锐角
|
|
|
| Args:
|
| contour: OpenCV轮廓点
|
| num_points: 插值后的点数
|
|
|
| Returns:
|
| 平滑后的轮廓点
|
| """
|
| if len(contour) < 4:
|
| return contour
|
|
|
|
|
| points = contour.reshape(-1, 2)
|
| x = points[:, 0].astype(np.float32)
|
| y = points[:, 1].astype(np.float32)
|
|
|
|
|
| x = np.append(x, x[0])
|
| y = np.append(y, y[0])
|
|
|
|
|
| distances = np.zeros(len(x))
|
| for i in range(1, len(x)):
|
| dx = x[i] - x[i-1]
|
| dy = y[i] - y[i-1]
|
| distances[i] = distances[i-1] + np.sqrt(dx*dx + dy*dy)
|
|
|
|
|
| if distances[-1] > 0:
|
| t = distances / distances[-1]
|
| else:
|
| return contour
|
|
|
|
|
| t_new = np.linspace(0, 1, num_points)
|
|
|
|
|
| try:
|
| from scipy.interpolate import interp1d
|
|
|
| fx = interp1d(t, x, kind='cubic', bounds_error=False, fill_value='extrapolate')
|
| fy = interp1d(t, y, kind='cubic', bounds_error=False, fill_value='extrapolate')
|
|
|
| x_new = fx(t_new)
|
| y_new = fy(t_new)
|
|
|
|
|
| smoothed = np.array([[int(x_new[i]), int(y_new[i])] for i in range(len(x_new))], dtype=np.int32)
|
| return smoothed.reshape(-1, 1, 2)
|
| except ImportError:
|
|
|
| print(" 警告: 未安装scipy,使用线性插值代替样条插值")
|
| fx = np.interp(t_new, t, x)
|
| fy = np.interp(t_new, t, y)
|
| smoothed = np.array([[int(fx[i]), int(fy[i])] for i in range(len(fx))], dtype=np.int32)
|
| return smoothed.reshape(-1, 1, 2)
|
|
|
|
|
| def contour_to_ring(contour, geo_transform):
|
| """将OpenCV轮廓转换为OGR线性环"""
|
| ring = ogr.Geometry(ogr.wkbLinearRing)
|
| for point in contour:
|
| x_pixel = point[0][0]
|
| y_pixel = point[0][1]
|
|
|
|
|
| x_geo = geo_transform[0] + x_pixel * geo_transform[1] + y_pixel * geo_transform[2]
|
| y_geo = geo_transform[3] + x_pixel * geo_transform[4] + y_pixel * geo_transform[5]
|
|
|
| ring.AddPoint(x_geo, y_geo)
|
|
|
|
|
| if ring.GetPointCount() > 0:
|
| first_point = ring.GetPoint(0)
|
| ring.AddPoint(first_point[0], first_point[1])
|
|
|
| return ring
|
|
|
|
|
| feature_count = 0
|
| processed_indices = set()
|
|
|
| for i, contour in enumerate(contours):
|
| if i in processed_indices or len(contour) < 3:
|
| continue
|
|
|
|
|
| parent_idx = hierarchy[0][i][3]
|
| if parent_idx != -1:
|
| continue
|
|
|
|
|
| processed_contour = contour
|
|
|
|
|
| if use_spline and len(contour) >= 4:
|
| processed_contour = smooth_contour_with_spline(contour, spline_points)
|
|
|
|
|
| approx = cv2.approxPolyDP(processed_contour, contour_approx_epsilon, closed=True)
|
|
|
| if len(approx) < 3:
|
| continue
|
|
|
|
|
| exterior_ring = contour_to_ring(approx, geo_transform)
|
|
|
|
|
| poly = ogr.Geometry(ogr.wkbPolygon)
|
| poly.AddGeometry(exterior_ring)
|
|
|
|
|
|
|
| child_idx = hierarchy[0][i][2]
|
| while child_idx != -1:
|
| if child_idx < len(contours):
|
| child_contour = contours[child_idx]
|
| if len(child_contour) >= 3:
|
|
|
| processed_child = child_contour
|
| if use_spline and len(child_contour) >= 4:
|
| processed_child = smooth_contour_with_spline(child_contour, spline_points)
|
|
|
|
|
| child_approx = cv2.approxPolyDP(processed_child, contour_approx_epsilon, closed=True)
|
| if len(child_approx) >= 3:
|
| interior_ring = contour_to_ring(child_approx, geo_transform)
|
| poly.AddGeometry(interior_ring)
|
| processed_indices.add(child_idx)
|
|
|
| child_idx = hierarchy[0][child_idx][0]
|
| else:
|
| break
|
|
|
|
|
| if check_topology:
|
|
|
| if not poly.IsValid():
|
|
|
| try:
|
| poly_fixed = poly.Buffer(0)
|
| if poly_fixed.IsValid():
|
| poly = poly_fixed
|
| print(f" ✓ 修复无效几何(轮廓 {i})")
|
| else:
|
| print(f" ✗ 无法修复无效几何(轮廓 {i}),跳过")
|
| processed_indices.add(i)
|
| continue
|
| except Exception as e:
|
| print(f" ✗ 修复几何失败(轮廓 {i}): {e},跳过")
|
| processed_indices.add(i)
|
| continue
|
|
|
|
|
| area = poly.GetArea()
|
| if area <= 0:
|
| print(f" ✗ 跳过零面积几何(轮廓 {i})")
|
| processed_indices.add(i)
|
| continue
|
|
|
|
|
| if min_area > 0 and area < min_area:
|
| print(f" ✗ 跳过面积过小的几何(轮廓 {i},面积: {area:.2f} 平方米)")
|
| processed_indices.add(i)
|
| continue
|
|
|
|
|
| feature = ogr.Feature(out_layer.GetLayerDefn())
|
| feature.SetGeometry(poly)
|
| feature.SetField(field_name, target_value)
|
| out_layer.CreateFeature(feature)
|
| feature = None
|
| feature_count += 1
|
| processed_indices.add(i)
|
|
|
|
|
| out_ds = None
|
| ds = None
|
|
|
| print(f"✅ 矢量化完成,共 {feature_count} 个面要素")
|
| print(f" 输出: {out_shp_path}")
|
|
|
| return out_shp_path
|
|
|
|
|
|
|
|
|
|
|
| def stitch_binary_predictions_patch_final(output_dir, json_filename, output_filename="stitched_prediction.png"):
|
| """
|
| 拼接切片为 PNG 文件,确保与 'patch00000_prediction.png' 命名规则匹配。
|
| (代码与上一个回答的最终版本一致,这里仅作为整合)
|
| """
|
| json_filepath = os.path.join(output_dir, json_filename)
|
| print(f"--- 🚀 步骤 I: 拼接二值图 ---")
|
|
|
| try:
|
| with open(json_filepath, 'r') as f:
|
| data = json.load(f)
|
| except Exception as e:
|
| raise RuntimeError(f"❌ 错误: 读取或解析JSON文件出错: {e}")
|
|
|
| tile_results = data.get('tile_results', [])
|
| if not tile_results:
|
| print("⚠️ 警告: JSON文件中没有找到 'tile_results' 数据。")
|
| return None
|
|
|
| def get_tile_filename(tile_id):
|
| return f"patch{tile_id:05d}_prediction.png"
|
|
|
| first_tile_id = tile_results[0]['tile_id']
|
| first_tile_path = os.path.join(output_dir, get_tile_filename(first_tile_id))
|
|
|
| try:
|
| with Image.open(first_tile_path) as img:
|
| tile_width, tile_height = img.size
|
| except Exception as e:
|
| raise RuntimeError(f"❌ 致命错误: 无法读取第一个切片 {first_tile_path}。请确认文件名。")
|
|
|
| max_x = max(item['x'] for item in tile_results)
|
| max_y = max(item['y'] for item in tile_results)
|
| stitched_width = max_x + tile_width
|
| stitched_height = max_y + tile_height
|
|
|
| print(f"✅ 切片尺寸 (W x H): {tile_width} x {tile_height} | 预计大图尺寸: {stitched_width} x {stitched_height}")
|
|
|
|
|
| estimated_memory_mb = (stitched_width * stitched_height * 1) / (1024 * 1024)
|
| print(f"📊 预计内存需求: {estimated_memory_mb:.2f} MB")
|
|
|
| if estimated_memory_mb > 10000:
|
| print("⚠️ 警告: 图像很大,可能需要大量内存")
|
|
|
| try:
|
| stitched_image = Image.new('L', (stitched_width, stitched_height))
|
| except MemoryError:
|
| error_msg = (f"❌ 内存不足: 无法创建 {stitched_width}x{stitched_height} 的图像。\n"
|
| f" 预计需要约 {estimated_memory_mb:.2f} MB 内存。\n"
|
| f" 建议: 1) 关闭其他程序 2) 使用更大的内存 3) 分块处理")
|
| raise RuntimeError(error_msg)
|
| except Exception as e:
|
| raise RuntimeError(f"❌ 创建拼接图像失败: {e}")
|
|
|
| print(f"⏳ 开始拼接 {len(tile_results)} 个切片...")
|
| processed_count = 0
|
|
|
| for tile_info in tile_results:
|
| tile_id = tile_info['tile_id']
|
| x_offset = tile_info['x']
|
| y_offset = tile_info['y']
|
| tile_path = os.path.join(output_dir, get_tile_filename(tile_id))
|
|
|
| try:
|
| with Image.open(tile_path) as tile_img:
|
| if tile_img.mode != 'L':
|
| tile_img = tile_img.convert('L')
|
| stitched_image.paste(tile_img, (x_offset, y_offset))
|
| processed_count += 1
|
| if processed_count % 1000 == 0:
|
| print(f" 已处理 {processed_count}/{len(tile_results)} 个切片...")
|
| except FileNotFoundError:
|
| print(f"⚠️ 警告: 找不到切片图像文件,跳过: {tile_path}")
|
| except Exception as e:
|
| print(f"❌ 处理切片 {tile_id} 时出错,跳过: {e}")
|
|
|
| print(f"✅ 完成拼接,共处理 {processed_count} 个切片")
|
|
|
| final_output_path = os.path.join(output_dir, output_filename)
|
|
|
|
|
| print(f"⏳ 正在保存拼接结果到 {output_filename}...")
|
| try:
|
|
|
| stitched_image.save(final_output_path, format='PNG', compress_level=1)
|
| print(f"✅ PNG 拼接完成,保存至: {final_output_path}")
|
| except MemoryError:
|
|
|
| print("⚠️ 直接保存失败,尝试使用分块保存...")
|
| try:
|
|
|
| stitched_image.save(final_output_path, format='PNG', compress_level=9, optimize=True)
|
| print(f"✅ PNG 拼接完成(使用压缩),保存至: {final_output_path}")
|
| except Exception as e:
|
| raise RuntimeError(f"❌ 保存失败: {e}\n"
|
| f" 图像太大 ({stitched_width}x{stitched_height}),内存不足。\n"
|
| f" 建议: 1) 关闭其他程序 2) 增加虚拟内存 3) 使用分块处理")
|
| except Exception as e:
|
| raise RuntimeError(f"❌ 保存PNG文件失败: {e}")
|
|
|
|
|
| stitched_image = None
|
|
|
| return final_output_path
|
|
|
|
|
|
|
|
|
| if __name__ == '__main__':
|
|
|
|
|
| PREDICTION_OUTPUT_DIR = "outputs/test_large"
|
|
|
| JSON_FILENAME = "GF6_PMS_E121.1_N33.6_20250604_L1A1420584616-MUX_fuse_tile_results.json"
|
|
|
|
|
| ORIGINAL_TIFF_PATH = r"datu/test/GF6_PMS_E121.1_N33.6_20250604_L1A1420584616-MUX_fuse.tif"
|
|
|
|
|
| STITCHED_PNG_FILENAME = "stitched_prediction.png"
|
| OUTPUT_GEOTIFF_FILENAME = "final_binary_prediction.tif"
|
| OUTPUT_SHP_FILENAME = "output_vector_test/sargassum_polygon.shp"
|
|
|
|
|
| SIEVE_PIXELS = 0
|
|
|
|
|
| try:
|
|
|
| stitched_png_path = stitch_binary_predictions_patch_final(
|
| PREDICTION_OUTPUT_DIR,
|
| JSON_FILENAME,
|
| STITCHED_PNG_FILENAME
|
| )
|
| if not stitched_png_path:
|
| sys.exit(1)
|
|
|
|
|
| stitched_tiff_path = create_geotiff_from_png(
|
| stitched_png_path,
|
| ORIGINAL_TIFF_PATH,
|
| os.path.join(PREDICTION_OUTPUT_DIR, OUTPUT_GEOTIFF_FILENAME)
|
| )
|
|
|
|
|
| raster2polygon(
|
| in_raster_path=stitched_tiff_path,
|
| out_shp_path=os.path.join(PREDICTION_OUTPUT_DIR, OUTPUT_SHP_FILENAME),
|
| sieve_size=SIEVE_PIXELS,
|
| connected_8=True
|
| )
|
|
|
| except Exception as e:
|
| import traceback
|
| print(f"\n❌ 致命错误发生: {e}")
|
| print("\n详细错误信息:")
|
| traceback.print_exc()
|
| print("\n可能的原因:")
|
| print("1. 文件路径不正确或文件不存在")
|
| print("2. 内存不足(图像太大)")
|
| print("3. 文件权限问题")
|
| print("4. GDAL库配置问题")
|
| sys.exit(1)
|
|
|