AI如何“慧眼識珠”進行計數呢?

在競爭日益激烈的制造業與電商領域,每一分成本都至關重要。您是否還在為產品計數環節而困擾?

  • 高價值小零件(如螺絲、珠寶、電子元件)的人工計數,效率低下且易出錯?

  • 藥品、保健品瓶裝前的計數,對精度有嚴苛要求,容不得半點馬虎?

  • 海量零散物品的分裝與包裝,人工成本高昂,管理困難?

人工計數的時代,該落幕了。?是時候讓更智能、更可靠的伙伴——視覺計數包裝機,來接管這項繁瑣而關鍵的任務。

核心技術揭秘:AI的“火眼金睛”是怎樣煉成的?

許多人好奇,這臺機器是如何像人眼一樣,甚至比人眼更精準地識別并數出成千上萬的物體的?其核心,在于融合了尖端計算機視覺深度學習AI的智能系統。整個過程,可以概括為以下四個精密的步驟:

第一步:高清捕捉,“明察秋毫”
系統首先通過工業級高分辨率攝像頭,在均勻穩定的光源環境下,對傳送帶或振動盤上的待計數產品進行快速連續拍照。這確保了獲取的圖片清晰、無陰影、無畸變,為AI的精準分析打下堅實基礎。

第二步:智能識別,“去偽存真”
這是AI大顯身手的環節。經過海量數據訓練的深度學習模型,會對圖片進行如下分析:

  • 特征提取:?AI模型能夠自動學習并識別目標物體的獨特特征,如形狀、大小、顏色、紋理、邊緣輪廓等。無論是圓形的藥片、方形的芯片還是異形的螺絲,它都能精準捕捉其本質特征。

  • 目標檢測與分割:?AI會像一位經驗豐富的老師傅,迅速在圖片中“圈出”每一個獨立的物體,哪怕它們有部分重疊或堆積。先進的算法能夠智能地將粘連的物體區分開來,極大降低了誤判率。

  • 分類過濾:?系統可以設定規則,自動忽略背景干擾、灰塵或與目標物形態迥異的雜質,確保只計數正確的產品,實現“去偽存真”。

第三步:精準計數,“分毫不錯”
在成功識別出每一個物體后,AI會對其進行實時標記。系統會以驚人的速度對標記框進行統計,無論是成千上萬的零部件,還是細如發絲的元器件,都能在瞬間完成計數,速度遠超人工,且精度高達99.9%以上,徹底告別人工計數的誤差與爭議。

AI如何“慧眼識珠”進行計數呢?

從像素到數據:圖像識別計數AI的底層邏輯與算法革新:

/// <summary>
/// 暗區域檢測數據集 - 自動加載圖像和標注文件進行訓練
/// 支持多種標注格式并包含針對暗區域的專用數據增強
/// </summary>
public class DarkRegionDataset : IEnumerable<Dictionary<string, Tensor>>, IDisposable
{
private readonly string[] imageFilePaths; // 圖像文件路徑數組
private readonly string[] annotationFilePaths; // 標注文件路徑數組
private readonly DarkRegionDetectorConfig config; // 訓練配置參數
private readonly Random randomGenerator; // 隨機數生成器,用于數據增強
private readonly int inputImageSize; // 輸入圖像尺寸
private bool isDisposed = false; // 資源釋放標志

/// <summary>
/// 構造函數 - 初始化數據集并驗證數據完整性
/// </summary>
public DarkRegionDataset(string imagesDirectory, string annotationsDirectory, DarkRegionDetectorConfig config)
{
this.config = config; // 保存配置參數
this.randomGenerator = new Random(DateTime.Now.Millisecond); // 初始化隨機數生成器
this.inputImageSize = 640; // 設置輸入圖像尺寸為640x640

// 加載圖像文件路徑
this.imageFilePaths = Directory.GetFiles(imagesDirectory, "*.jpg") // 獲取所有jpg文件
.Concat(Directory.GetFiles(imagesDirectory, "*.png")) // 獲取所有png文件
.Concat(Directory.GetFiles(imagesDirectory, "*.bmp")) // 獲取所有bmp文件
.OrderBy(path => path) // 按路徑排序確保一致性
.ToArray(); // 轉換為數組

// 加載標注文件路徑
this.annotationFilePaths = Directory.GetFiles(annotationsDirectory, "*.txt") // 獲取所有txt標注文件
.OrderBy(path => path) // 按路徑排序
.ToArray(); // 轉換為數組

// 驗證數據完整性
ValidateDatasetIntegrity(); // 檢查圖像和標注文件是否匹配
Console.WriteLine($"數據集加載完成: {imageFilePaths.Length} 張圖像, {annotationFilePaths.Length} 個標注文件"); // 輸出加載信息
}

/// <summary>
/// 驗證數據集完整性 - 檢查圖像和標注文件是否匹配
/// </summary>
private void ValidateDatasetIntegrity()
{
if (imageFilePaths.Length != annotationFilePaths.Length) // 檢查數量是否一致
{
throw new InvalidDataException($"圖像文件數量({imageFilePaths.Length})與標注文件數量({annotationFilePaths.Length})不匹配"); // 拋出異常
}

// 檢查文件名是否對應
for (int i = 0; i < imageFilePaths.Length; i++) // 遍歷所有文件
{
string imageName = Path.GetFileNameWithoutExtension(imageFilePaths[i]); // 獲取圖像文件名(不含擴展名)
string annotationName = Path.GetFileNameWithoutExtension(annotationFilePaths[i]); // 獲取標注文件名(不含擴展名)

if (imageName != annotationName) // 檢查文件名是否一致
{
throw new InvalidDataException($"文件不匹配: {imageName} 與 {annotationName}"); // 拋出異常
}
}
}

/// <summary>
/// 獲取數據集大小
/// </summary>
public int Count => imageFilePaths.Length; // 返回圖像文件數量

/// <summary>
/// 索引器 - 通過索引獲取單個數據樣本
/// </summary>
public Dictionary<string, Tensor> this[int index]
{
get
{
if (index < 0 || index >= imageFilePaths.Length) // 檢查索引有效性
throw new IndexOutOfRangeException($"索引 {index} 超出范圍 [0, {imageFilePaths.Length - 1}]");

return LoadSingleSample(index); // 加載單個樣本
}
}

/// <summary>
/// 加載單個樣本 - 讀取圖像和標注并執行預處理
/// </summary>
private Dictionary<string, Tensor> LoadSingleSample(int index)
{
// 加載并預處理圖像
Tensor processedImage = LoadAndPreprocessImage(imageFilePaths[index]); // 加載和預處理圖像

// 加載并解析標注
Tensor processedAnnotations = LoadAndParseAnnotations(annotationFilePaths[index]); // 加載和解析標注

// 應用數據增強(訓練時)
if (config.EnableDarknessEnhancement) // 如果啟用數據增強
{
(processedImage, processedAnnotations) = ApplyTrainingAugmentations(processedImage, processedAnnotations); // 應用數據增強
}

// 返回樣本字典
return new Dictionary<string, Tensor>
{
{ "image", processedImage }, // 處理后的圖像張量
{ "target", processedAnnotations } // 處理后的標注張量
};
}

/// <summary>
/// 加載和預處理圖像 - 讀取圖像文件并轉換為模型輸入格式
/// </summary>
private Tensor LoadAndPreprocessImage(string imagePath)
{
// 使用System.Drawing加載圖像
using (var bitmap = new Bitmap(imagePath)) // 加載位圖文件
{
// 轉換為RGB格式(確保3通道)
using (var rgbBitmap = new Bitmap(bitmap.Width, bitmap.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb)) // 創建RGB位圖
{
using (var graphics = Graphics.FromImage(rgbBitmap)) // 創建繪圖對象
{
graphics.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height); // 繪制原圖像
}

// 將Bitmap轉換為Tensor
Tensor imageTensor = BitmapToTensor(rgbBitmap); // 轉換位圖為張量

// 應用圖像預處理
imageTensor = PreprocessImageTensor(imageTensor); // 預處理圖像張量

return imageTensor; // 返回處理后的張量
}
}
}

/// <summary>
/// 將Bitmap轉換為Tensor - 圖像數據轉換為PyTorch張量格式
/// </summary>
private Tensor BitmapToTensor(Bitmap bitmap)
{
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), // 鎖定位圖數據
System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat); // 只讀模式

try
{
int bytesPerPixel = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8; // 計算每像素字節數
byte[] pixelData = new byte[bitmapData.Stride * bitmap.Height]; // 創建像素數據數組
Marshal.Copy(bitmapData.Scan0, pixelData, 0, pixelData.Length); // 復制非托管數據到托管數組

// 將字節數據轉換為float張量
Tensor tensor = torch.zeros(new long[] { bitmap.Height, bitmap.Width, 3 }, torch.float32); // 創建空張量

for (int y = 0; y < bitmap.Height; y++) // 遍歷所有行
{
for (int x = 0; x < bitmap.Width; x++) // 遍歷所有列
{
int index = y * bitmapData.Stride + x * bytesPerPixel; // 計算像素索引

// 讀取BGR值并轉換為RGB
float b = pixelData[index] / 255.0f; // 藍色通道,歸一化到[0,1]
float g = pixelData[index + 1] / 255.0f; // 綠色通道,歸一化到[0,1]
float r = pixelData[index + 2] / 255.0f; // 紅色通道,歸一化到[0,1]

tensor[y, x, 0] = r; // 紅色通道
tensor[y, x, 1] = g; // 綠色通道
tensor[y, x, 2] = b; // 藍色通道
}
}

return tensor; // 返回圖像張量
}
finally
{
bitmap.UnlockBits(bitmapData); // 解鎖位圖數據
}
}

/// <summary>
/// 圖像預處理 - 調整尺寸、歸一化等操作
/// </summary>
private Tensor PreprocessImageTensor(Tensor image)
{
// 調整圖像尺寸到目標大小
image = functional.interpolate(image.unsqueeze(0), // 添加批次維度并插值
new long[] { inputImageSize, inputImageSize }, // 目標尺寸
mode: InterpolationMode.Bilinear, // 雙線性插值
align_corners: false).squeeze(0); // 移除批次維度

// 如果配置為單通道輸入,轉換為灰度圖
if (config.InputChannels == 1) // 檢查是否需要單通道
{
image = ConvertToGrayscale(image); // 轉換為灰度圖
}

// 歸一化到[0,1]范圍(如果尚未歸一化)
if (image.max().item<float>() > 1.0f) // 檢查是否已經歸一化
{
image = image / 255.0f; // 歸一化到[0,1]
}

// 調整維度順序為 [C, H, W]
image = image.permute(new long[] { 2, 0, 1 }); // 從[H,W,C]變為[C,H,W]

return image; // 返回預處理后的圖像
}

/// <summary>
/// 轉換為灰度圖 - 將RGB圖像轉換為單通道灰度圖
/// </summary>
private Tensor ConvertToGrayscale(Tensor rgbImage)
{
// 使用標準灰度轉換公式: Y = 0.299R + 0.587G + 0.114B
Tensor grayscale = 0.299f * rgbImage[":", ":", 0] + // 紅色分量
0.587f * rgbImage[":", ":", 1] + // 綠色分量
0.114f * rgbImage[":", ":", 2]; // 藍色分量

return grayscale.unsqueeze(2); // 添加通道維度 [H, W, 1]
}

/// <summary>
/// 加載和解析標注 - 讀取標注文件并轉換為模型目標格式
/// </summary>
private Tensor LoadAndParseAnnotations(string annotationPath)
{
var annotations = new List<float[]>(); // 創建標注列表

if (File.Exists(annotationPath)) // 檢查標注文件是否存在
{
string[] lines = File.ReadAllLines(annotationPath); // 讀取所有行
foreach (string line in lines) // 遍歷每一行
{
if (string.IsNullOrWhiteSpace(line)) // 跳過空行
continue;

string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); // 分割字符串
if (parts.Length >= 5) // 檢查格式是否正確(class x_center y_center width height)
{
float classId = float.Parse(parts[0]); // 類別ID
float xCenter = float.Parse(parts[1]); // 中心點x坐標(歸一化)
float yCenter = float.Parse(parts[2]); // 中心點y坐標(歸一化)
float width = float.Parse(parts[3]); // 寬度(歸一化)
float height = float.Parse(parts[4]); // 高度(歸一化)

annotations.Add(new float[] { classId, xCenter, yCenter, width, height }); // 添加到列表
}
}
}

// 轉換為Tensor格式
if (annotations.Count > 0) // 如果有標注
{
Tensor annotationTensor = torch.zeros(new long[] { annotations.Count, 5 }, torch.float32); // 創建標注張量
for (int i = 0; i < annotations.Count; i++) // 遍歷所有標注
{
annotationTensor[i] = torch.tensor(annotations[i]); // 設置每一行數據
}
return annotationTensor; // 返回標注張量
}
else // 如果沒有標注(負樣本)
{
return torch.zeros(new long[] { 0, 5 }, torch.float32); // 返回空標注
}
}

/// <summary>
/// 應用訓練時數據增強 - 提高模型泛化能力
/// </summary>
private (Tensor image, Tensor annotations) ApplyTrainingAugmentations(Tensor image, Tensor annotations)
{
Tensor augmentedImage = image.clone(); // 克隆圖像,避免修改原始數據
Tensor augmentedAnnotations = annotations.clone(); // 克隆標注

// 隨機水平翻轉(50%概率)
if (config.EnableHorizontalFlip && randomGenerator.NextDouble() > 0.5) // 檢查是否啟用并隨機決定
{
(augmentedImage, augmentedAnnotations) = ApplyHorizontalFlip(augmentedImage, augmentedAnnotations); // 應用水平翻轉
}

// 隨機亮度調整
if (randomGenerator.NextDouble() > 0.5) // 50%概率應用亮度調整
{
augmentedImage = AdjustBrightness(augmentedImage, config.LuminanceAdjustment); // 調整亮度
}

// 隨機對比度調整
if (randomGenerator.NextDouble() > 0.5) // 50%概率應用對比度調整
{
augmentedImage = AdjustContrast(augmentedImage, config.ContrastVariation); // 調整對比度
}

// 針對暗區域的特殊增強
if (config.EnableDarknessEnhancement) // 如果啟用暗區域增強
{
augmentedImage = EnhanceDarkRegions(augmentedImage); // 增強暗區域
}

return (augmentedImage, augmentedAnnotations); // 返回增強后的數據和標注
}

/// <summary>
/// 應用水平翻轉 - 同時翻轉圖像和調整標注坐標
/// </summary>
private (Tensor image, Tensor annotations) ApplyHorizontalFlip(Tensor image, Tensor annotations)
{
// 翻轉圖像(在寬度維度)
Tensor flippedImage = functional.pad(image, new long[] { 0, 0, 0, 0 }, mode: PaddingModes.Reflect); // 填充
flippedImage = torch.flip(flippedImage, new long[] { 2 }); // 沿寬度維度翻轉

// 調整標注坐標
if (annotations.shape[0] > 0) // 如果有標注
{
Tensor flippedAnnotations = annotations.clone(); // 克隆標注
flippedAnnotations[":", 1] = 1.0f - flippedAnnotations[":", 1]; // 翻轉x中心坐標
annotations = flippedAnnotations; // 更新標注
}

return (flippedImage, annotations); // 返回翻轉后的圖像和標注
}

/// <summary>
/// 調整亮度 - 隨機改變圖像亮度
/// </summary>
private Tensor AdjustBrightness(Tensor image, float maxAdjustment)
{
float adjustment = (float)(randomGenerator.NextDouble() * maxAdjustment * 2 - maxAdjustment); // 隨機亮度調整量
return torch.clamp(image + adjustment, 0.0f, 1.0f); // 應用調整并限制范圍
}

/// <summary>
/// 調整對比度 - 隨機改變圖像對比度
/// </summary>
private Tensor AdjustContrast(Tensor image, float maxFactor)
{
float factor = (float)(1.0 + randomGenerator.NextDouble() * maxFactor * 2 - maxFactor); // 隨機對比度因子
Tensor mean = image.mean(); // 計算圖像均值
return torch.clamp((image - mean) * factor + mean, 0.0f, 1.0f); // 應用對比度調整
}

/// <summary>
/// 增強暗區域 - 專門針對暗區域的對比度增強
/// </summary>
private Tensor EnhanceDarkRegions(Tensor image)
{
// 創建暗區域掩碼(像素值低于閾值)
Tensor darkMask = image < config.DarknessThreshold; // 暗區域掩碼

if (darkMask.any().item<bool>()) // 如果存在暗區域
{
// 增強暗區域對比度
Tensor enhancedDark = image * 1.5f; // 增強暗區域
enhancedDark = torch.clamp(enhancedDark, 0.0f, 1.0f); // 限制范圍

// 應用掩碼:只增強暗區域
image = torch.where(darkMask, enhancedDark, image); // 條件替換
}

return image; // 返回增強后的圖像
}

/// <summary>
/// 實現迭代器接口 - 支持foreach遍歷
/// </summary>
public IEnumerator<Dictionary<string, Tensor>> GetEnumerator()
{
for (int i = 0; i < imageFilePaths.Length; i++) // 遍歷所有樣本
{
yield return this[i]; // 返回當前樣本
}
}

/// <summary>
/// 顯式接口實現 - 返回非泛型迭代器
/// </summary>
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator(); // 返回泛型迭代器
}

/// <summary>
/// 釋放資源 - 實現IDisposable接口
/// </summary>
public void Dispose()
{
if (!isDisposed) // 如果尚未釋放
{
// 這里可以釋放任何非托管資源
isDisposed = true; // 標記為已釋放
GC.SuppressFinalize(this); // 阻止終結器調用
}
}
}
2. 暗區域訓練器(完整訓練流程)
csharp
/// <summary>
/// 暗區域檢測訓練器 - 管理完整的模型訓練流程
/// 包含訓練循環、驗證、模型保存和進度監控
/// </summary>
public class DarkRegionTrainer : IDisposable
{
private DarkRegionDetector model; // 暗區域檢測模型
private optim.Optimizer modelOptimizer; // 模型優化器
private DarkRegionDetectionLoss lossFunction; // 損失函數
private DarkRegionDetectorConfig trainingConfig; // 訓練配置
private Device trainingDevice; // 訓練設備(CPU/GPU)
private LearningRateScheduler learningRateScheduler; // 學習率調度器
private bool isDisposed = false; // 資源釋放標志

/// <summary>
/// 訓練進度事件 - 用于報告訓練進度和指標
/// </summary>
public event Action<TrainingProgress> TrainingProgressUpdated;

/// <summary>
/// 構造函數 - 初始化訓練器的所有組件
/// </summary>
public DarkRegionTrainer(DarkRegionDetectorConfig config)
{
this.trainingConfig = config; // 保存訓練配置
InitializeTrainingDevice(); // 初始化訓練設備
InitializeModelComponents(); // 初始化模型和優化器
InitializeLearningRateScheduler(); // 初始化學習率調度器

Console.WriteLine($"訓練器初始化完成,使用設備: {trainingDevice}"); // 輸出初始化信息
}

/// <summary>
/// 初始化訓練設備 - 自動選擇CPU或GPU
/// </summary>
private void InitializeTrainingDevice()
{
if (torch.cuda.is_available()) // 檢查CUDA是否可用
{
trainingDevice = Device.CUDA; // 使用GPU
Console.WriteLine("檢測到CUDA設備,使用GPU進行訓練"); // 輸出GPU信息
}
else // 如果沒有GPU
{
trainingDevice = Device.CPU; // 使用CPU
Console.WriteLine("未檢測到CUDA設備,使用CPU進行訓練"); // 輸出CPU信息
}
}

/// <summary>
/// 初始化模型組件 - 創建模型、損失函數和優化器
/// </summary>
private void InitializeModelComponents()
{
// 初始化暗區域檢測模型
this.model = new DarkRegionDetector(trainingConfig, trainingDevice, ScalarType.Float32); // 創建模型

// 初始化損失函數,針對暗區域檢測優化參數
this.lossFunction = new DarkRegionDetectionLoss(
darkRegionWeight: 2.0f, // 暗區域權重較高
positiveSampleWeight: 1.0f, // 正樣本標準權重
negativeSampleWeight: 0.5f // 負樣本權重較低
);

// 將模型和損失函數移動到訓練設備
model.to(trainingDevice); // 移動模型到設備
lossFunction.to(trainingDevice); // 移動損失函數到設備

// 初始化優化器,使用Adam優化器
var trainableParameters = model.parameters().Where(param => param.requires_grad).ToList(); // 獲取可訓練參數
this.modelOptimizer = optim.Adam(
trainableParameters, // 可訓練參數列表
trainingConfig.InitialLearningRate, // 初始學習率
weight_decay: trainingConfig.RegularizationStrength // 權重衰減
);

Console.WriteLine($"模型初始化完成,可訓練參數: {trainableParameters.Count}"); // 輸出模型信息
}

/// <summary>
/// 初始化學習率調度器 - 動態調整學習率
/// </summary>
private void InitializeLearningRateScheduler()
{
// 使用余弦退火學習率調度
this.learningRateScheduler = optim.lr_scheduler.CosineAnnealingLR(
modelOptimizer, // 優化器
T_max: trainingConfig.TotalEpochs, // 總周期數
eta_min: trainingConfig.InitialLearningRate * 0.01f // 最小學習率
);
}

/// <summary>
/// 執行完整訓練流程 - 包含訓練和驗證
/// </summary>
public void ExecuteTraining(string trainingImagesPath, string trainingAnnotationsPath,
string validationImagesPath = null, string validationAnnotationsPath = null)
{
// 加載訓練數據集
using (var trainingDataset = new DarkRegionDataset(trainingImagesPath, trainingAnnotationsPath, trainingConfig)) // 創建訓練數據集
{
DarkRegionDataset validationDataset = null; // 驗證數據集

// 如果有驗證數據,加載驗證集
if (!string.IsNullOrEmpty(validationImagesPath) && !string.IsNullOrEmpty(validationAnnotationsPath)) // 檢查驗證路徑
{
validationDataset = new DarkRegionDataset(validationImagesPath, validationAnnotationsPath, trainingConfig); // 創建驗證數據集
Console.WriteLine($"驗證集加載完成: {validationDataset.Count} 個樣本"); // 輸出驗證集信息
}

// 創建數據加載器
using (var trainingDataLoader = new DataLoader(trainingDataset, trainingConfig.BatchSize, shuffle: true)) // 訓練數據加載器
{
// 執行訓練循環
for (int currentEpoch = 0; currentEpoch < trainingConfig.TotalEpochs; currentEpoch++) // 遍歷所有訓練周期
{
// 執行單個訓練周期
float epochLoss = ExecuteSingleTrainingEpoch(trainingDataLoader, currentEpoch); // 訓練一個周期

// 如果有驗證集,執行驗證
float validationLoss = 0f;
if (validationDataset != null) // 如果有驗證集
{
using (var validationDataLoader = new DataLoader(validationDataset, trainingConfig.BatchSize, shuffle: false)) // 驗證數據加載器
{
validationLoss = ExecuteValidationEpoch(validationDataLoader, currentEpoch); // 執行驗證
}
}

// 更新學習率
learningRateScheduler.step(); // 調整學習率

// 報告訓練進度
ReportTrainingProgress(currentEpoch, epochLoss, validationLoss); // 報告進度

// 定期保存模型檢查點
if ((currentEpoch + 1) % 10 == 0 || currentEpoch == trainingConfig.TotalEpochs - 1) // 每10個周期或最后周期
{
SaveModelCheckpoint(currentEpoch, epochLoss, validationLoss); // 保存檢查點
}
}
}

// 釋放驗證數據集
validationDataset?.Dispose(); // 如果存在驗證集,釋放資源
}

Console.WriteLine("訓練完成!"); // 輸出完成信息
}

/// <summary>
/// 執行單個訓練周期 - 遍歷整個訓練集并更新模型參數
/// </summary>
private float ExecuteSingleTrainingEpoch(DataLoader trainingLoader, int epochNumber)
{
model.train(); // 設置模型為訓練模式
float totalEpochLoss = 0f; // 累計損失
int processedBatches = 0; // 已處理批次計數

Console.WriteLine($"開始訓練周期 {epochNumber + 1}/{trainingConfig.TotalEpochs}"); // 輸出周期開始信息

foreach (var batchData in trainingLoader) // 遍歷所有訓練批次
{
// 清空梯度
modelOptimizer.zero_grad(); // 清零梯度

繼續閱讀
我的微信
這是我的微信掃一掃
weinxin
我的微信
微信號已復制
我的微信公眾號
我的微信公眾號掃一掃
weinxin
我的公眾號
公眾號已復制
 

發表評論