news 2026/10/5 3:43:45

高分遥感语义分割PyTorch实战:从数据准备到DeepLabV3+训练推理全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高分遥感语义分割PyTorch实战:从数据准备到DeepLabV3+训练推理全指南

简介:面向深度学习与遥感交叉应用的PyTorch语义分割项目,聚焦高分遥感影像地物分类任务。资源包含858个文件,以819张PNG遥感影像、标注及预测结果图为主体,另含35个Python脚本、1个CSV标签文件、说明文档与示例图片,整体约547.74MB。Python脚本覆盖模型训练、推理预测、膨胀预测及后处理流程,并加入半监督伪标签方法与tensorboardX可视化输出,配套CSV记录影像对应地物标签信息,方便对照分析。已有3041人学习下载,适合需要系统掌握高分遥感语义分割全流程的研究者或开发者。通过该包可获取从数据预处理、模型构建到结果可视化的完整实现,大量分割结果对比图有助于直观评估算法效果,并可作为同类地物分类项目的改造基础。

1. 高分遥感语义分割用PyTorch落地:从像素到地物分类的完整链路

语义分割在自然图像里已经不算新鲜事,但换成高分遥感影像做地物分类,几乎是另一套玩法。这个人工智能项目实践的核心,是用PyTorch训练一个能逐像素区分建筑、道路、水体、植被、裸地的模型,输入是亚米级的高分影像,输出是一张和影像同尺寸的分割图。地物分类在意的不只是"图里有什么",而是"每一个像素属于什么",所以它在土地调查、违建监测、农业估产里都是基础设施级的算法能力。适合读这篇的人:已经会PyTorch基础操作、手头有遥感数据却不知道从哪一步开始的开发者,以及被课程项目或业务需求推着必须短期跑通闭环的工程师。

2. 数据准备与标签生成:高分影像的掩膜怎么造才不返工

遥感语义分割的成败,八成在数据。很多人的第一个翻车点不是模型,而是标签和影像对不上、类别定义模糊、切块后目标被拦腰切断。这一章按"摸清影像底细→矢量标签栅格化→切块与增强"的顺序讲,每一步给出能直接跑的脚本和参数说明。

2.1 高分遥感影像的分辨率、波段与内存预算

高分遥感影像和普通照片最大的区别是尺寸和波段。以资源三号、高分二号这类数据为例,单景影像轻松上亿像素,且通常是GeoTIFF格式,自带地理坐标。用OpenCV直接读GeoTIFF不是不行,但推荐用rasterio或GDAL,它们才能正确处理坐标系和波段顺序。

波段方面,RGB三通道对大部分地物分类已够用,但水体和水生植被在近红外波段的区分度明显更好。如果你的影像带近红外波段,建议用RGBN四通道训练,而不是强行降成RGB,代价是模型第一层卷积输入要改。先算显存账:一张512×512×3的float32输入只占约3MB,但训练时激活值、梯度、优化器状态要乘好几倍,batch size为8时模型总占用轻松超过10GB。所以数据准备阶段就要设计好切块尺寸,不能等训练时报OOM再回头改。

2.2 矢量shp转像素mask:栅格化的正确姿势

地物分类的标签通常不是现成的像素标注,而是测绘部门给的矢量边界。把矢量转成和影像严格对齐的像素mask是第一道工序。常见做法是geopandas读shp,rasterio的features.rasterize做栅格化,关键点是使用原影像的地理变换,而不是自己算行列号。

import numpy as np import rasterio import geopandas as gpd from rasterio.features import rasterize def shp_to_mask(shp_path, tif_path, output_path, class_map=None): """ class_map: dict,例如 {'building': 1, 'road': 2, 'water': 3} """ with rasterio.open(tif_path) as src: transform = src.transform crs = src.crs height, width = src.height, src.width gdf = gpd.read_file(shp_path) # 坐标系不一致是错位的头号原因,必须统一 if gdf.crs != crs: gdf = gdf.to_crs(crs) shapes = [] for _, row in gdf.iterrows(): cls = row.get('class', None) or row.get('type', None) if cls in class_map: shapes.append((row.geometry, class_map[cls])) mask = rasterize( shapes, out_shape=(height, width), transform=transform, fill_value=0, dtype=np.uint8 ) with rasterio.open( output_path, 'w', driver='GTiff', height=height, width=width, count=1, dtype='uint8', crs=crs, transform=transform ) as dst: dst.write(mask, 1)

逻辑说明:先从原始GeoTIFF中读取地理变换、坐标系和尺寸,再读取矢量文件,若坐标系不一致就转换到影像坐标系。rasterize按矢量几何逐像素写入,fill_value=0表示背景类别。写入时同样带上原影像的crs和transform,这样输出的mask和影像天然对齐。

参数说明:class_map中的键必须和shp属性表里的字段值完全一致,值就是模型训练时用的类别索引。如果shp里有多个类别但某个类别占比极小,可以先合并到相近类别,避免训练时样本不平衡。另一个坑是矢量文件中有重叠多边形,rasterize默认后画的覆盖先画的,所以写入shapes的顺序最好按类别优先级排好。

2.3 影像切块与数据增强:用有限的样本撑起训练集

整景影像不能直接进GPU,必须切成patch。两个参数需要权衡:patch_size和stride。patch_size常见取256或512,目标尺度偏大就选512,小目标多就选256。stride小于patch_size时产生重叠,既能缓解目标被切断的问题,也相当于数据扩充,代价是训练样本重复、训练时间变长。

def crop_image_label(image, mask, patch_size=512, stride=256): crops = [] h, w = image.shape[:2] for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): crops.append(( image[y:y + patch_size, x:x + patch_size], mask[y:y + patch_size, x:x + patch_size] )) return crops

切完的patch建议直接存成npy格式,而不是训练时反复读GeoTIFF。大文件随机读取的IO开销在数据加载阶段会被放大十几倍,npy能显著提升DataLoader的吞吐量。下面是一个同步增强函数,影像和mask必须用一致的随机变换,否则像素对应关系会错位。

import random import numpy as np def train_augmentation(image, mask): # 同步随机翻转,保证image和mask保持一致 if random.random() > 0.5: image = image[:, ::-1] mask = mask[:, ::-1] if random.random() > 0.5: image = image[::-1] mask = mask[::-1] # 轻度亮度调整,只改image、不动mask brightness = np.random.uniform(0.9, 1.1) image = np.clip(image * brightness, 0, 255).astype(np.uint8) return image, mask

注意:遥感影像的增强不要用随机擦除、剧烈颜色抖动这类强变换,它们会破坏地物的光谱特征,导致模型学到虚假的纹理规律。旋转90度、翻转、轻度亮度对比度调整是最稳妥的组合。

3. 模型选型与训练代码:用PyTorch搭建DeepLabV3+地物分类基线

模型选择直接决定迭代速度。语义分割模型从FCN到U-Net、DeepLab系列、SegFormer候选很多,但对高分遥感地物分类,选型逻辑和自然图像不太一样,需要同时看精度、显存和收敛速度。

3.1 FCN、U-Net、DeepLabV3+怎么选:遥感场景的取舍

FCN是语义分割的鼻祖,结构简单但上采样粗糙,对地物边界极不友好,现在用它做遥感基本是浪费时间。U-Net在小数据集、医学图像和遥感小目标场景里很稳,编码器-解码器结构让细节保留得好,而且结构直观方便改。DeepLabV3+用空洞卷积和ASPP模块,在不牺牲分辨率的前提下扩大感受野,对道路这类狭长结构和大型建筑表现好,是地物分类最常用的基线之一。SegFormer等Transformer类模型精度高,但显存占用和训练成本都翻倍,资源有限时不建议第一版就用。

我的建议是第一次跑通用DeepLabV3+配ResNet50,精度和速度的平衡最好,PyTorch自带的torchvision实现五分钟能搭起来。如果类别里小目标极多,比如要分电线杆、井盖,再换U-Net或加细节增强模块。

3.2 用torchvision搭DeepLabV3+:最小可用代码

PyTorch官方torchvision直接提供deeplabv3_resnet50,无需自己手写backbone和ASPP,只需替换分类头输出通道数。

import torch import torch.nn as nn from torchvision.models.segmentation import deeplabv3_resnet50 def build_model(num_classes=5, freeze_backbone=False): model = deeplabv3_resnet50(weights='DEFAULT') # 替换分类头,输出通道数改为当前任务的类别数 in_channels = model.classifier[4].in_channels model.classifier[4] = nn.Conv2d(in_channels, num_classes, kernel_size=1) if freeze_backbone: for param in model.backbone.parameters(): param.requires_grad = False return model

说明:weights='DEFAULT'加载ImageNet预训练权重,遥感影像虽然分布不同,但预训练能显著加速收敛,尤其当只有几千张训练patch时。freeze_backbone=True可用于分段训练:先冻结backbone只训练分类头,等loss不再下降再解冻整体微调,这在遥感数据上很实用。

注意:torchvision分割模型的输出是字典,训练时取model(image)['out'],而不是直接当成tensor。如果你的PyTorch版本较老,weights参数要换成pretrained=True。

3.3 训练循环主框架与参数说明

from torch.utils.data import Dataset, DataLoader class SegDataset(Dataset): def __init__(self, image_files, mask_files): self.image_files = image_files self.mask_files = mask_files def __len__(self): return len(self.image_files) def __getitem__(self, idx): image = np.load(self.image_files[idx]) mask = np.load(self.mask_files[idx]) image, mask = train_augmentation(image, mask) image_t = torch.from_numpy(image).permute(2, 0, 1).float() / 255.0 mask_t = torch.from_numpy(mask).long() return image_t, mask_t train_loader = DataLoader( SegDataset(train_images, train_masks), batch_size=8, shuffle=True, num_workers=4, pin_memory=True ) device = 'cuda' if torch.cuda.is_available() else 'cpu' model = build_model().to(device) criterion = nn.CrossEntropyLoss(ignore_index=255) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scaler = torch.cuda.amp.GradScaler() for epoch in range(50): model.train() for images, masks in train_loader: images = images.to(device) masks = masks.to(device) optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs = model(images)['out'] loss = criterion(outputs, masks) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()

参数说明:ignore_index=255用于跳过标签中未标注的区域,标注时留空的像素值设为255即可,模型不会计算这些位置的损失。混合精度缩放器能把显存占用降低约40%,batch size受限时很关键。数据加载用num_workers=4并行读npy文件,如果内存吃紧可以调低或改用prefetch_factor限制预取。

一个容易被忽略的细节:这里的image假设已归一化到0-1,如果直接用0-255的uint8喂进模型,预训练backbone里的BatchNorm统计量会彻底错乱,收敛极慢。归一化必须在数据集里做,不能靠模型内部处理。

3.4 损失函数与评估指标:mIoU才是地物分类的真正标尺

交叉熵是默认选择,但遥感地物分类类别极不平衡,单独交叉熵会让模型把所有像素都判给背景。两个补救方案:给CrossEntropyLoss传类别权重,按样本像素比例反比计算;或用Dice Loss、Focal Loss与交叉熵加权组合。评估指标则不能用accuracy,它会被背景类别严重拉高,必须用mIoU。

import math def compute_miou(preds, masks, num_classes): ious = [] for cls in range(num_classes): pred_mask = (preds == cls) true_mask = (masks == cls) inter = (pred_mask & true_mask).sum() union = (pred_mask | true_mask).sum() if union == 0: ious.append(float('nan')) else: ious.append(inter.item() / union.item()) valid = [v for v in ious if not math.isnan(v)] return sum(valid) / len(valid), ious

说明:验证阶段逐类统计交并比,忽略测试集中不存在的类别,避免虚高或虚低。实际交付时不能只报一个mIoU,每类IoU要单独列出,建筑和道路这类关键地物的单类IoU往往比总指标更被业务方在意。

4. 训练配置与调参:让遥感分割模型真正收敛

这个环节最玄学。同一个模型同一份数据,学习率差一倍可能从mIoU 0.55涨到0.67,也可能直接收敛成全背景。这章把环境、优化器、学习率和类别平衡这些关键旋钮一个个说清楚。

4.1 PyTorch环境搭建:CUDA版本和torch版本怎么对

先装PyTorch,常见问题是装成CPU版而不自知。如果在conda里无脑pip install torch,很可能装到CPU版本,训练一天才发现显卡没干活。稳妥做法是先去PyTorch官网复制对应CUDA版本的安装命令,CUDA版本要看显卡驱动支持的版本,而不是系统里装了什么CUDA toolkit。

# 查看显卡驱动支持的CUDA版本 nvidia-smi # 检查torch是否真正用上了CUDA python -c "import torch; print(torch.cuda.is_available(), torch.version.cuda)"

如果torch.cuda.is_available()返回False,先确认nvidia-smi有输出。有输出说明驱动正常,大概率是torch装了CPU版,重装GPU版即可。Ubuntu上安装PyTorch时注意别把系统Python和conda环境混用,我的血泪经验是混用后环境乱到两个月后自己都分不清哪个环境是哪个。远程服务器推荐用WSL2做PyTorch环境搭建,CUDA透传很省心,但WSL2里nvidia-smi显示的显存和任务管理器的数值不一致是正常的。

4.2 优化器与学习率调度:AdamW加poly策略是遥感分割的常见组合

分类任务常用SGD加CosineAnnealing,但分割任务像素密度大、梯度噪声大,AdamW首轮收敛更快。等模型进入稳定区后,再配合poly衰减做长尾收敛。poly策略是DeepLab系列论文里的做法:学习率按公式lr = init_lr * (1 - iter / total_iters)^power衰减。

def poly_lr(optimizer, init_lr, current_iter, total_iters, power=0.9): lr = init_lr * (1 - current_iter / total_iters) ** power for param_group in optimizer.param_groups: param_group['lr'] = lr

说明:power取0.9是论文原值,衰减速度适中。如果训练只有二三十个epoch,可以改成1.0做线性衰减。另一个常用做法是前500步做warmup,把学习率从0线性升到目标值,避免初始大步长把预训练权重破坏掉。学习率的选择上,ResNet50编码器配lr=1e-4基本安全,如果换成MobileNet等轻量backbone可以试5e-4。batch size变化时按线性缩放:bs从8变到16,学习率翻倍。

4.3 类别不平衡:当背景占了九成像素时怎么办

遥感地物分类几乎一定有类别不平衡,建筑和道路只占少量像素,背景或植被占大头。直接交叉熵会让模型把所有不确定像素都判给大头类别,小类别IoU接近0。最直接的做法是给loss加权重,按像素占比的倒数计算。

def compute_class_weight(mask_files, num_classes): pixel_count = np.zeros(num_classes) for f in mask_files: mask = np.load(f) for cls in range(num_classes): pixel_count[cls] += int((mask == cls).sum()) total = pixel_count.sum() weights = total / (pixel_count * num_classes) weights = weights / weights.mean() # 归一化到均值1附近 return torch.from_numpy(weights).float() criterion = nn.CrossEntropyLoss( weight=weights.to(device), ignore_index=255 )

说明:权重归一化能让loss整体量级不偏移太多,避免训练震荡。类别表示的像素数为0时要单独处理,不然会出现除零。权重根据训练集计算一次后固定使用,不要每个epoch动态更新,否则loss数值不稳定、难以对比收敛进度。

如果加了权重仍救不回来,检查小类别是不是在切块时被切碎了。比如道路,256×256的窗口经常把整条路切成几段,每片里道路占比都很小,模型学不到完整结构。这时把patch_size调大到512,或者做重叠率更高的随机切块。

5. 高分遥感语义分割的常见坑与排查记录

这章是实战踩坑汇总,按"现象→原因→解决"的顺序写,每条都是我在真实项目里遇到过的。

5.1 显存报错:OOM却不一定是batch size的锅

训练刚起步就报CUDA out of memory,nvidia-smi一看显存占用才2GB/11GB。原因是PyTorch显存按需申请加缓存,OOM发生在某个batch的前向或后向瞬间,这时峰值显存超了剩余空间。解决:先把batch size从32降成8,打开AMP混合精度;再把num_workers调低,避免数据加载和训练争抢CPU资源。如果还不行,检查是否有其他进程占显存,用fuser -v /dev/nvidia*找出不相关的进程清掉。另外torch.backends.cudnn.benchmark=True对遥感影像这种固定输入尺寸的任务很有用,它自动选最优卷积算法,能省约10%显存和时间,但要放在模型和输入进GPU之前设置。

5.2 掩膜错位:标签和影像差了"几十米"

mask和影像叠加显示时地物边界整体偏移。最常见原因有两个:shp和tif坐标系不一致,比如shp是WGS84经纬度、tif用了UTM投影;或者栅格化时没用原影像的transform而自己算行列号。解决:shp读进来后先to_crs到影像坐标系,然后用src.transform栅格化。这类错误训练阶段不报错,但mIoU会诡异地在0.7附近波动,因为模型拿偏移的标签当标准答案,永远学不对。

5.3 mIoU虚高:验证集切分方式决定的泛化假象

训练集和验证集来自同一景影像的随机切块,mIoU虚高到0.85,新影像上一测打回原形。原因是高分遥感影像相邻像素高度相关,随机切块后训练集和验证集实际上大量重叠,模型记住的是那片区域的纹理细节,不是泛化的地物规律。解决:按影像而不是按像素切分,把整景影像分成几块,一部分训练、一部分验证、一部分测试,让验证集和训练集来自不同地理位置。这是遥感分割和自然图像分割最大的差异,也是最容易被忽视的泛化陷阱。

注意:遥感分割的验证集绝对不能按随机像素划分,必须按影像地理位置划分,否则mIoU虚高没有任何参考价值。

5.4 椒盐噪声与锯齿边缘:后处理要克制

预测结果里背景中零散出现小预测块,建筑边界像锯齿。原因一是patch边缘预测不稳定,二是没有后处理。解决:推理时用重叠滑动窗口,重叠区域取概率均值;最后做一次形态学滤波,开运算去除小噪点、闭运算填补建筑孔洞。但后处理要克制,过度开闭运算会抹掉真实细节,比如小水体被当噪点清掉,在业务上反而更糟。

5.5 训练loss不降:从标签类型和类别编号开始排查

loss在0.5附近震荡,预测全输出背景。多数情况是标签类别值写错了:mask类别从1开始,但模型输出是0到num_classes-1,交叉熵要求mask像素值是类别索引而不是原始编号。检查class_map里是不是把1、2、3直接当成了label而没做索引映射。另一个常见原因是mask被转成了float,交叉熵要求LongTensor,类型不匹配时PyTorch会静默广播,训练完全失效,这类问题肉眼很难发现,建议在Dataset里强制.long()并打印几个样本核对。

6. 瓦片式推理与精度验证:把模型输出变成能交付的结果

训练结束后的一公里才是交付的关键。单景高分影像动辄几万乘几万像素,不能一次性前向推理,要用重叠滑动窗口。这章给出推理脚本和验证思路。

6.1 重叠滑动窗口推理:接缝消失的边界处理

def slide_inference(model, image, patch_size=512, stride=256, num_classes=5, device='cuda'): h, w = image.shape[:2] result = np.zeros((h, w, num_classes), dtype=np.float32) count = np.zeros((h, w, 1), dtype=np.float32) model.eval() with torch.no_grad(): for y in range(0, h, stride): for x in range(0, w, stride): y1, x1 = min(y + patch_size, h), min(x + patch_size, w) ph, pw = y1 - y, x1 - x patch = image[y:y1, x:x1] # 边界不足时填充到patch_size if ph < patch_size or pw < patch_size: padded = np.zeros((patch_size, patch_size, image.shape[2]), dtype=np.uint8) padded[:ph, :pw] = patch patch = padded inp = torch.from_numpy(patch).permute(2, 0, 1).float().unsqueeze(0).to(device) / 255.0 prob = torch.softmax(model(inp)['out'], dim=1)[0].cpu().numpy().transpose(1, 2, 0) result[y:y1, x:x1] += prob[:ph, :pw] count[y:y1, x:x1] += 1 return (result / (count + 1e-8)).argmax(-1).astype(np.uint8)

说明:重叠区域累加概率再取平均,patch边缘的不连续接缝会自然消失。stride取patch_size一半时重叠最多、结果最稳,但推理时间翻倍;追求速度时stride可以等于patch_size,代价是接缝可能更明显。推理务必在model.eval()和torch.no_grad()下进行,否则Dropout和BatchNorm的不确定性会把结果搞花。

6.2 精度验证与后续迭代:mIoU之外还要看什么

预测结果要导出成和原影像地理对齐的GeoTIFF才能进GIS系统,用rasterio写入时复制原影像的transform和crs即可。验证指标分两级:像素级看mIoU,图斑级做连通域分析,统计每个地物图斑的面积和位置与真实矢量的重叠度。业务上往往更在意图斑级精度,一个建筑漏判的影响远大于边界误差几米。

训练完第一版,我的习惯是专门拉出错误预测做误报分析:把预测结果半透明叠加在影像上,逐类看建筑混淆是发生在阴影区还是屋顶材质区、道路误判多出现在水泥地还是树荫下。这些结论直接指导要不要补数据、换模型、改后处理。后续要加新类别时,在现有模型上解冻分类层、降低学习率微调,比从头训练省一半时间。跑了三轮项目后我最大的教训是:遥感分割项目的时间分配,数据准备和验证各占三成,模型训练只占两成,剩下两成在处理"数据为什么不干净"。手里有多少张有效patch、标签和影像对不对齐,比选什么模型更决定项目成败。希望这篇能帮你在第一次跑通时少走几条弯路,把时间花在真正难啃的地物边界上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 3:41:24

OpenShell 使用指南:把 Win10/Win11 开始菜单变回经典样式

如果你是从 Windows XP/7 时代一路用过来的老用户&#xff0c;第一次打开 Win10/Win11 自带开始菜单时&#xff0c;心里大概都会咯噔一下&#xff1a;磁贴铺满一屏&#xff0c;程序散落各处&#xff0c;想找控制面板还得先点一列菜单。这时候 OpenShell 就能派上用场了。简单说…

作者头像 李华
网站建设 2026/10/5 3:41:22

基于PHP/MySQL/WebSocket的轻量级在线客服系统设计与二次开发

做在线客服系统这件事&#xff0c;我前前后后折腾了不少年。市面上现成的客服产品要么重得要命&#xff0c;部署一台机器都嫌挤&#xff1b;要么是商业SaaS&#xff0c;功能是齐全&#xff0c;但想改个欢迎语、接个内部工单系统&#xff0c;处处受限制。所以我一直很推崇那种“…

作者头像 李华
网站建设 2026/10/5 3:41:02

基于小程序的水上警务通:设计与开发全解析

每年毕业季&#xff0c;计算机专业的毕设选题里有一批“老熟人”&#xff1a;商城、食堂订餐、图书馆预约、健身房管理。这些不是不能做&#xff0c;但答辩撞车率实在太高。今天聊的这个题目&#xff0c;光看名字就跟别人拉开差距了——基于小程序的水上警务通设计与开发。我第…

作者头像 李华
网站建设 2026/10/5 3:39:20

MySQL锁机制与死锁排查实战:从原理到生产优化

做后端开发和数据库运维的同学&#xff0c;几乎都遇到过这种场景&#xff1a;凌晨两点被监控电话吵醒&#xff0c;工单上写着“Deadlock found when trying to get lock; try restarting transaction”&#xff0c;或者某个接口的P99延迟从50毫秒飙到5秒&#xff0c;数据库连接…

作者头像 李华
网站建设 2026/10/5 3:39:15

openrig开源模拟赛车座舱:铝型材DIY搭建与调校全指南

openrig 这个项目&#xff0c;严格来说不是某一个作者开一个仓库发一套图纸那么简单。在模拟赛车这个圈子里&#xff0c;rig 指的是整套驾驶舱设备&#xff0c;包括座椅、转向机、踏板、显示器和整体框架&#xff1b;open 则意味着从 CAD 模型、BOM 清单到安装步骤全部开放&…

作者头像 李华
网站建设 2026/10/5 3:38:47

Java局域网聊天室系统设计与实现:从Socket编程到线程安全

简介&#xff1a;这是一份面向毕业设计与课程设计的Java局域网聊天室系统完整资料&#xff0c;适合计算机相关专业学生和初学Java网络编程的开发者使用。内容包含ChatClient与ChatServer两部分的源代码、配套论文以及工程配置文件&#xff0c;覆盖Socket通信、多线程、界面交互…

作者头像 李华