news 2026/9/28 2:06:11

茶叶叶片病害图像分类实战:4000张标注数据从清洗到部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
茶叶叶片病害图像分类实战:4000张标注数据从清洗到部署全流程

简介:本资源为常规茶叶叶片病害图像分类数据集,面向从事图像分类、农业病害识别方向的学生与算法工程师,可用于训练与评估CNN分类网络。数据集已标注,共划分5个类别,包括褐枯病、灰枯萎病、红点病等,具体类别名称可查看包内json文件。资源已按训练集、验证集、测试集划分,各类别图片分别存放于对应目录,便于直接加载训练;同时提供show脚本,可快速可视化样本分布与图像内容。压缩包共2000个文件,以1998张jpg图像为主,另含1个py脚本与1个json标注文件,整体约21.68MB,体积轻量、结构清晰。目前已有104人浏览学习,适合作为课程设计、毕业设计或分类网络改进实验的基准数据,配合作者主页的CNN改进与图像分类项目,可进一步完成模型对比与调参验证。

1. 茶叶叶片病害图像分类数据集:4000 张标注图能跑出什么结果

茶叶叶片病害识别这件事,真正卡住大多数团队的从来不是模型结构,而是数据。你手上如果有一份约 4,000 张、已经标注好的常规茶叶叶片病害图像分类数据集,那么从零到能出第一版可用模型,其实只需要一个下午。问题在于,很多人拿到数据集之后第一反应是直接ImageFolder一把梭,训完发现验证集准确率 98%,换一批自己拍的叶子照片就崩到 60% 以下——这不是模型的问题,是没搞清楚这份数据集的分布、标注粒度和采集条件。

这份数据集的核心价值在于「已标注」三个字。图像分类数据集和检测数据集不一样,它不需要边界框,每张图对应一个类别标签,目录结构通常就是类别名/图片文件。约 4,000 张的规模,放在图像分类任务里属于中小体量,够训一个从零开始的轻量网络,也够微调一个预训练 backbone。它适合谁?适合做农业物联网终端识别的嵌入式工程师、做茶园巡检机器人的算法同学、以及想拿一个真实场景数据集练手图像分类全流程的开发者。下面我从数据检查、划分、训练到部署验证,把这条链路拆开讲。

2. 先别急着训:4000 张茶叶病害图的清洗与分布核查

2.1 目录结构确认与类别统计

拿到数据集第一件事不是写训练脚本,而是把目录结构和类别分布摸清楚。茶叶叶片病害常见类别包括茶饼病、茶炭疽病、茶白星病、茶轮斑病以及健康叶片,但不同数据集标注体系不一样,有的把「虫害」和「病害」混在一起,有的只标了「病斑」和「健康」两类。你得先确认这份数据集的类别定义。

import os from collections import Counter from pathlib import Path # 假设数据集根目录结构为 dataset/train/类别名/xxx.jpg # 先不管 train/val 划分,直接扫描所有图片 root = Path("tea_leaf_dataset") exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} class_count = Counter() corrupt_files = [] for cls_dir in sorted(root.iterdir()): if not cls_dir.is_dir(): continue for img in cls_dir.rglob("*"): if img.suffix.lower() in exts: class_count[cls_dir.name] += 1 # 顺手检查文件大小,0 字节基本是坏图 if img.stat().st_size < 1024: corrupt_files.append(str(img)) print("类别分布:") for k, v in class_count.most_common(): print(f" {k}: {v} 张") print(f"总图片数: {sum(class_count.values())}") print(f"疑似损坏文件: {len(corrupt_files)}") for f in corrupt_files[:10]: print(" ", f)

这段脚本做三件事:统计每个类别的图片数量、检查小于 1KB 的疑似坏图、输出总样本数。参数上,exts集合按你实际拿到的格式增减,rglob("*")会递归子目录,防止有人把图片又套了一层文件夹。如果类别之间数量差距超过 3:1,后面训练必须做重采样或加权损失,否则模型会偏向多数类。

2.2 用感知哈希找出重复图和近似图

4,000 张的数据集里,重复图和近似图是准确率虚高的头号元凶。同一片叶子换个角度拍两张、或者从视频里连续抽帧,都会让训练集和验证集出现信息泄漏。我一般用感知哈希(pHash)快速筛一遍。

import imagehash from PIL import Image from pathlib import Path from collections import defaultdict root = Path("tea_leaf_dataset") hashes = defaultdict(list) for img_path in root.rglob("*"): if img_path.suffix.lower() not in {".jpg", ".jpeg", ".png"}: continue try: with Image.open(img_path) as im: # hash_size=8 得到 64 位指纹,对轻微旋转/缩放不敏感 h = str(imagehash.phash(im, hash_size=8)) hashes[h].append(str(img_path)) except Exception as e: print(f"跳过 {img_path}: {e}") # 输出重复组 dup_groups = {k: v for k, v in hashes.items() if len(v) > 1} print(f"发现 {len(dup_groups)} 组重复/近似图") for h, files in list(dup_groups.items())[:5]: print(f" 指纹 {h}: {len(files)} 张") for f in files: print(" ", f)

imagehash.phash的hash_size参数控制指纹精度,8 表示 8×8 的感知哈希,对 JPEG 压缩和轻微亮度变化鲁棒。如果两组图指纹完全相同,基本可以判定是重复。处理策略:同一组只保留一张,其余删掉或移到_duplicates备份目录。这一步做完,你的有效样本可能从 4,000 降到 3,500 左右,但模型泛化会明显更稳。

2.3 训练/验证/测试划分的三个硬约束

划分不是随机train_test_split就完事。茶叶叶片数据集的划分要满足三个约束:第一,同一片叶子的多张照片必须落在同一个集合,否则验证集准确率会虚高;第二,类别比例在三个集合中要一致;第三,测试集要留出至少 15%,且全程不参与任何调参。

import random from pathlib import Path from collections import defaultdict import shutil random.seed(42) root = Path("tea_leaf_dataset") out = Path("tea_split") splits = {"train": 0.7, "val": 0.15, "test": 0.15} # 按类别收集,假设已人工确认无重复图 for cls_dir in sorted(root.iterdir()): if not cls_dir.is_dir(): continue imgs = [p for p in cls_dir.rglob("*") if p.suffix.lower() in {".jpg", ".png", ".jpeg"}] random.shuffle(imgs) n = len(imgs) n_train = int(n * splits["train"]) n_val = int(n * splits["val"]) assigned = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:], } for split, files in assigned.items(): dst = out / split / cls_dir.name dst.mkdir(parents=True, exist_ok=True) for f in files: shutil.copy2(f, dst / f.name) print(f"{cls_dir.name}: train={len(assigned['train'])}, val={len(assigned['val'])}, test={len(assigned['test'])}")

random.seed(42)保证可复现,shutil.copy2保留原文件元数据。如果你的数据集已经按叶子 ID 分好组,把imgs换成按组划分即可。划分完检查一下每个 split 下每个类别的数量,差距超过 10% 就重新调种子或手动补。

3. 从 ImageFolder 到 DataLoader:茶叶叶片分类的输入管线怎么搭

3.1 用 torchvision 搭最小可跑管线

图像分类的输入管线看起来简单,但茶叶叶片这个场景有几个特殊点:叶片在画面中的位置不固定、背景可能是茶园土壤或手指、光照从顺光到逆光都有。所以增强策略不能照搬 ImageNet 那套。

import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.Resize((256, 256)), # 先统一到稍大尺寸 transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), # 随机裁剪,模拟不同拍摄距离 transforms.RandomHorizontalFlip(p=0.5), transforms.RandomVerticalFlip(p=0.3), # 叶片旋转在真实场景常见 transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.05), transforms.RandomRotation(degrees=25), # 手机拍摄角度不固定 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("tea_split/train", transform=train_tf) val_ds = datasets.ImageFolder("tea_split/val", transform=val_tf) test_ds = datasets.ImageFolder("tea_split/test", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) test_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) print("类别映射:", train_ds.class_to_idx) print(f"训练集 {len(train_ds)} / 验证集 {len(val_ds)} / 测试集 {len(test_ds)}")

关键参数说明:RandomResizedCrop的scale=(0.6, 1.0)允许裁到原图 60% 区域,模拟近距离拍摄病斑;RandomVerticalFlip概率设 0.3 而不是 0.5,因为叶片正反面在真实场景中不是等概率出现;ColorJitter的hue只给 0.05,茶叶的绿色色调是重要特征,色相扰动太大会把病斑颜色改掉。num_workers在 Windows 上如果报错就设 0,Linux 下按 CPU 核数一半设置。

3.2 类别不均衡时的 WeightedRandomSampler

如果第 2 章统计出来某个类别只有 200 张、最多的有 1,200 张,直接训会让模型几乎只预测多数类。除了加权损失,更稳的做法是用WeightedRandomSampler在采样阶段就做平衡。

import numpy as np from torch.utils.data import WeightedRandomSampler # 基于训练集标签计算每个类别的权重 targets = [label for _, label in train_ds.samples] class_counts = np.bincount(targets) print("训练集类别计数:", class_counts) # 每个样本的权重 = 1 / 该类样本数 class_weights = 1.0 / class_counts sample_weights = class_weights[targets] sampler = WeightedRandomSampler( weights=torch.DoubleTensor(sample_weights), num_samples=len(sample_weights), replacement=True, ) train_loader_balanced = DataLoader( train_ds, batch_size=32, sampler=sampler, num_workers=4, pin_memory=True, )

replacement=True表示有放回采样,少数类会被反复抽到。num_samples设成和训练集一样大,保证每个 epoch 的步数不变。注意用了sampler就不能再设shuffle=True,两者互斥。这个方案比在损失函数里加weight更直接,因为它在数据层面就平衡了梯度贡献。

3.3 验证集增强的边界:什么时候不该加

验证集和测试集只能用Resize + CenterCrop + Normalize,不能加任何随机增强。我见过有人在验证集上也加RandomHorizontalFlip,结果每次验证准确率波动 3 个点,调参完全没法判断。验证集的唯一作用是衡量模型在当前数据分布下的表现,加随机性等于引入噪声。如果你发现验证集准确率远高于测试集,先检查是不是验证集图片和训练集有重复,而不是去调增强参数。

4. 选 backbone 还是从零训:4000 张图上的模型策略与训练参数

4.1 小数据集上的迁移学习 vs 从零训练

4,000 张图在图像分类里属于「微调够用、从零偏少」的区间。从零训一个 ResNet-18 大概能到 85% 左右,但需要仔细调学习率和正则;用 ImageNet 预训练权重微调,通常能到 92% 以上,而且收敛快得多。我的建议是:除非你的部署环境对模型结构有硬性限制,否则一律走迁移学习。

import torch.nn as nn from torchvision import models def build_model(num_classes, pretrained=True, freeze_backbone=False): # 用 ResNet-18 做 baseline,轻量且够用 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None) if freeze_backbone: # 冻结除 fc 外的所有层 for param in model.parameters(): param.requires_grad = False # 替换分类头 in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes), ) return model num_classes = len(train_ds.classes) model = build_model(num_classes, pretrained=True, freeze_backbone=False) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) print(f"类别数: {num_classes}, 设备: {device}")

freeze_backbone=False表示全网络微调。如果你的数据量更小(比如每类不到 200 张),可以先设True只训分类头 5 个 epoch,再解冻全网络用更小的学习率微调。Dropout(0.3)放在全连接前,对小数据集防过拟合有效。

4.2 学习率、权重衰减与早停的实操参数

训练参数没有万能值,但茶叶叶片分类这个场景有一套比较稳的起点。

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR criterion = nn.CrossEntropyLoss(label_smoothing=0.1) # 标签平滑,防过拟合 optimizer = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-6) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (outputs.argmax(1) == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) total_loss += loss.item() * imgs.size(0) correct += (outputs.argmax(1) == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total

lr=3e-4是微调预训练模型的常用起点,weight_decay=1e-4配合 AdamW 做解耦权重衰减。label_smoothing=0.1把硬标签软化,对标注可能有噪声的数据集特别有用——茶叶病斑的类别边界有时候连人都要犹豫。CosineAnnealingLR的T_max=30表示 30 个 epoch 内余弦下降到eta_min。

4.3 训练循环与最佳模型保存

把上面的组件串起来,加上早停和最佳模型保存。

best_val_acc = 0.0 patience, wait = 7, 0 epochs = 40 for epoch in range(epochs): train_loss, train_acc = train_one_epoch(model, train_loader_balanced, criterion, optimizer, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) scheduler.step() print(f"Epoch {epoch+1:02d} | train_loss={train_loss:.4f} acc={train_acc:.4f} | val_loss={val_loss:.4f} acc={val_acc:.4f}") if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_tea_model.pth") wait = 0 else: wait += 1 if wait >= patience: print(f"早停于 epoch {epoch+1},最佳验证准确率 {best_val_acc:.4f}") break

patience=7表示验证准确率连续 7 个 epoch 不提升就停。保存的是state_dict而不是整个模型,加载时更灵活。训练完在测试集上跑一次evaluate,那个数字才是你对外汇报的准确率。

5. 避坑与排查:茶叶叶片分类训练中最容易翻车的五件事

5.1 验证集准确率 99% 但实际部署全错

现象:训练日志里验证集准确率很快到 99%,但拿手机拍几张茶园照片测,几乎全部分错。

原因:验证集和训练集来自同一批采集数据,光照、背景、拍摄设备完全一致,模型学到的是「背景特征」而不是「病斑特征」。比如所有健康叶片都在白色背景下拍,模型可能把「白色背景」当成健康类的特征。

解决:从原始数据里按采集批次划分,而不是随机划分。如果数据集没有批次信息,至少按图片的亮度、背景颜色做分层抽样。更彻底的做法是留出一批完全独立采集的图片做测试集,哪怕只有 200 张。

5.2 损失不下降,准确率卡在随机水平

现象:训练 loss 从第一轮就不怎么降,准确率在类别数倒数附近徘徊。

原因:最常见的是标签映射错了。ImageFolder按文件夹名排序生成class_to_idx,如果你自己另外维护了一份标签映射,两边对不上,模型学的就是错标签。另一个可能是学习率太大,梯度直接炸了。

解决:打印train_ds.class_to_idx和你的标签映射对比。学习率从3e-4降到1e-4试一轮,如果 loss 开始降就是学习率问题。还可以检查Normalize的均值和方差是不是和预训练模型匹配,用错归一化参数会让输入分布偏移。

5.3 训练集准确率 100% 但验证集只有 70%

现象:训练集很快过拟合,验证集准确率上不去。

原因:数据增强太弱、模型容量太大、或者训练集和验证集分布差异大。4,000 张图训 ResNet-50 就比 ResNet-18 更容易过拟合。

解决:先加增强——RandomResizedCrop的 scale 下限降到 0.5,加RandomGrayscale(p=0.1)让模型不依赖颜色。再加正则——Dropout从 0.3 提到 0.5,weight_decay从1e-4提到5e-4。如果还不行,换更小的 backbone,ResNet-18 换成 MobileNetV3-Small。

5.4 推理时预处理和训练不一致

现象:训练时验证准确率 93%,导出模型部署后准确率掉到 60%。

原因:部署端的预处理和训练时不一致。训练用了Resize(256) + CenterCrop(224),部署时直接Resize(224),图像被拉伸变形。或者训练用了 ImageNet 归一化,部署时忘了减均值除方差。

解决:把验证集的val_tf完整复制到部署代码里,不要凭记忆重写。用同一张测试图分别在训练环境和部署环境跑一遍,对比预处理后的 tensor 数值,差超过 0.01 就是有问题。

5.5 类别标签顺序在部署时错位

现象:模型输出概率最高的类别索引是 2,但部署代码里索引 2 对应的类别名和训练时不一样。

原因:ImageFolder的class_to_idx是按文件夹名 ASCII 排序生成的,比如healthy排在anthracnose后面。部署时如果手动写了一个类别列表,顺序很容易错。

解决:训练完立刻把train_ds.class_to_idx保存成 JSON,部署时加载这个 JSON 做索引到类别名的映射,不要手写。

import json with open("class_mapping.json", "w", encoding="utf-8") as f: json.dump(train_ds.class_to_idx, f, ensure_ascii=False, indent=2) print("类别映射已保存:", train_ds.class_to_idx)

6. 把模型推到能用的程度:混淆矩阵、置信度阈值与一个部署前检查习惯

训练完拿到测试集准确率只是起点。我一般会做三件事来判断这个模型能不能真的拿去用。

第一件是画混淆矩阵,看错分集中在哪两个类之间。茶叶病害里茶炭疽病和茶轮斑病的病斑形态在早期很接近,如果这两个类互相错分率高,说明模型学到的特征还不够细。这时候可以针对性补充这两个类的样本,或者把输入分辨率从 224 提到 320,让病斑纹理更清晰。

import numpy as np from sklearn.metrics import confusion_matrix, classification_report @torch.no_grad() def get_all_preds(model, loader, device): model.eval() all_preds, all_labels = [], [] for imgs, labels in loader: imgs = imgs.to(device) outputs = model(imgs) preds = outputs.argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) return np.array(all_labels), np.array(all_preds) model.load_state_dict(torch.load("best_tea_model.pth", map_location=device)) y_true, y_pred = get_all_preds(model, test_loader, device) idx_to_class = {v: k for k, v in train_ds.class_to_idx.items()} target_names = [idx_to_class[i] for i in range(len(idx_to_class))] print(confusion_matrix(y_true, y_pred)) print(classification_report(y_true, y_pred, target_names=target_names, digits=4))

classification_report里的f1-score比准确率更能反映类别不均衡时的真实表现。如果某个类的 recall 低于 0.8,说明这个类的样本被大量漏判,需要补数据或调采样权重。

第二件是看模型对错误预测的置信度分布。一个可用的模型,错分时的置信度应该偏低;如果错分时置信度还是 0.95 以上,说明模型过自信,部署时不能只靠 argmax,要加置信度阈值。

import torch.nn.functional as F @torch.no_grad() def confidence_analysis(model, loader, device, threshold=0.7): model.eval() low_conf_count, wrong_high_conf = 0, 0 total = 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) probs = F.softmax(outputs, dim=1) max_probs, preds = probs.max(1) total += imgs.size(0) low_conf_count += (max_probs < threshold).sum().item() wrong_high_conf += ((preds != labels) & (max_probs >= 0.9)).sum().item() print(f"总样本 {total}") print(f"置信度低于 {threshold} 的样本: {low_conf_count} ({low_conf_count/total:.2%})") print(f"错分但置信度高于 0.9 的样本: {wrong_high_conf} ({wrong_high_conf/total:.2%})") confidence_analysis(model, test_loader, device)

如果「错分但高置信度」的比例超过 2%,部署时建议加一个兜底策略:置信度低于 0.7 的样本不自动判定,转人工复核。这在茶园巡检场景里很实用,宁可多拍一张让植保员看一眼,也不要自动给出错误结论。

第三件是做一个「部署前一致性检查」脚本,把训练时的验证集图片随机抽 20 张,走一遍部署端的完整推理流程,和训练时的预测结果对比。这个习惯帮我拦下过好几次预处理不一致的问题。

import random from PIL import Image def preprocess_for_deploy(img_path): # 这里必须和训练时的 val_tf 完全一致 tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) return tf(Image.open(img_path).convert("RGB")).unsqueeze(0) sample_imgs = random.sample(test_ds.samples, 20) model.eval() mismatch = 0 for img_path, true_label in sample_imgs: tensor = preprocess_for_deploy(img_path).to(device) with torch.no_grad(): pred = model(tensor).argmax(1).item() if pred != true_label: mismatch += 1 print(f"不一致: {img_path} 真实={idx_to_class[true_label]} 预测={idx_to_class[pred]}") print(f"20 张抽样中不一致 {mismatch} 张")

这 20 张里如果有超过 2 张和训练时预测不一致,说明部署端预处理有问题,回去逐行对比val_tf。

最后说一个我自己的习惯:每次训完模型,不管准确率多高,我都会拿手机去楼下绿化带拍 10 张不同光照条件下的叶子照片,手动过一遍模型。这 10 张不参与任何训练和调参,纯粹是「真实世界抽检」。如果这 10 张里错超过 3 张,测试集准确率再高我也不会把这个模型交出去。数据集是死的,茶园是活的,这个习惯帮我省了很多返工。希望帮到你。

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

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

Jetson Orin Nano 驱动树莓派摄像头:接口适配与设备树配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:06:04

收音机音频功放与电源优化实战:从SI4732到PAM8403的底噪治理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:06:02

AWD攻防赛脚本集合实战:从7z解压到Flag自动提交全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:04:47

超奈奎斯特(FTN)通信:用可控码间干扰突破频谱效率瓶颈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:04:14

SY8303 3A同步降压设计:热管理、损耗拆解与PCB布局硬核指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 2:03:31

C# UDP网口通信实战:工控场景下的可靠性设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华