使用CNN和Python實施的肺炎檢測


作者|Muhammad Ardi
編譯|Flin
來源|analyticsvidhya

介紹

嘿!幾個小時前我剛剛完成一個深度學習項目,現在我想分享一下我所做的事情。這一挑戰的目標是確定一個人是否患有肺炎。如果是,則確定是否由細菌或病毒引起。好吧,我覺得這個項目應該叫做分類而不是檢測。

換句話說,此任務將是一個多分類問題,其中標簽名稱為:normal(正常),virus(病毒)和bacteria(細菌)。為了解決這個問題,我將使用CNN(卷積神經網絡),它具有出色的圖像分類能力,。不僅如此,在這里我還實現了圖像增強技術,以提高模型性能。順便說一句,我獲得了80%的測試數據准確性,這對我來說是非常令人印象深刻的。

可以從該Kaggle鏈接下載此項目中使用的數據集。

整個數據集本身的大小約為1 GB,因此下載可能需要一段時間。或者,我們也可以直接創建一個Kaggle Notebook並在那里編碼整個項目,因此我們甚至不需要下載任何內容。接下來,如果瀏覽數據集文件夾,你將看到有3個子文件夾,即train,test和val。

好吧,我認為這些文件夾名稱是不言自明的。此外,train文件夾中的數據分別包括正常,病毒和細菌類別的1341、1345和2530個樣本。我想這就是我介紹的全部內容了,現在讓我們進入代碼的編寫!

注意:我在本文結尾處放置了該項目中使用的全部代碼。

加載模塊和訓練圖像

使用計算機視覺項目時,要做的第一件事是加載所有必需的模塊和圖像數據本身。我使用tqdm模塊顯示進度條,稍后你將看到它有用的原因。

我最后導入的是來自Keras模塊的ImageDataGenerator。該模塊將幫助我們在訓練過程中實施圖像增強技術。

import os
import cv2
import pickle
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import confusion_matrix
from keras.models import Model, load_model
from keras.layers import Dense, Input, Conv2D, MaxPool2D, Flatten
from keras.preprocessing.image import ImageDataGeneratornp.random.seed(22)

接下來,我定義兩個函數以從每個文件夾加載圖像數據。乍一看,下面的兩個功能可能看起來完全一樣,但是在使用粗體顯示的行上實際上存在一些差異。這樣做是因為NORMAL和PNEUMONIA文件夾中的文件名結構略有不同。盡管有所不同,但兩個功能執行的其他過程基本相同。

首先,將所有圖像調整為200 x 200像素。

這一點很重要,因為所有文件夾中的圖像都有不同的尺寸,而神經網絡只能接受具有固定數組大小的數據。

接下來,基本上所有圖像都存儲有3個顏色通道,這對X射線圖像來說是多余的。因此,我的想法是將這些彩色圖像都轉換為灰度圖像。

# Do not forget to include the last slash
def load_normal(norm_path):
    norm_files = np.array(os.listdir(norm_path))
    norm_labels = np.array(['normal']*len(norm_files))
    
    norm_images = []
    for image in tqdm(norm_files):
        image = cv2.imread(norm_path + image)
        image = cv2.resize(image, dsize=(200,200))
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        norm_images.append(image)
        
    norm_images = np.array(norm_images)
    
    return norm_images, norm_labels
def load_pneumonia(pneu_path):
    pneu_files = np.array(os.listdir(pneu_path))
    pneu_labels = np.array([pneu_file.split('_')[1] for pneu_file in pneu_files])
    
    pneu_images = []
    for image in tqdm(pneu_files):
        image = cv2.imread(pneu_path + image)
        image = cv2.resize(image, dsize=(200,200))
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        pneu_images.append(image)
        
    pneu_images = np.array(pneu_images)
    
    return pneu_images, pneu_labels

聲明了以上兩個函數后,現在我們可以使用它來加載訓練數據了。如果你運行下面的代碼,你還將看到為什么我選擇在該項目中實現tqdm模塊。

norm_images, norm_labels = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/train/NORMAL/')pneu_images, pneu_labels = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/train/PNEUMONIA/')

到目前為止,我們已經獲得了幾個數組:norm_images,norm_labels,pneu_images和pneu_labels。

帶_images后綴的表示它包含預處理的圖像,而帶_labels后綴的數組表示它存儲了所有基本信息(也稱為標簽)。換句話說,norm_images和pneu_images都將成為我們的X數據,其余的將成為y數據。

為了使項目看起來更簡單,我將這些數組的值連接起來並存儲在X_train和y_train數組中。

X_train = np.append(norm_images, pneu_images, axis=0)
y_train = np.append(norm_labels, pneu_labels)

順便說一句,我使用以下代碼獲取每個類的圖像數:

顯示多張圖像

好吧,在這個階段,顯示幾個圖像並不是強制性的。但我想做是為了確保圖片是否已經加載和預處理好。下面的代碼用於顯示14張從X_train陣列隨機拍攝的圖像以及標簽。

fig, axes = plt.subplots(ncols=7, nrows=2, figsize=(16, 4))

indices = np.random.choice(len(X_train), 14)
counter = 0

for i in range(2):
    for j in range(7):
        axes[i,j].set_title(y_train[indices[counter]])
        axes[i,j].imshow(X_train[indices[counter]], cmap='gray')
        axes[i,j].get_xaxis().set_visible(False)
        axes[i,j].get_yaxis().set_visible(False)
        counter += 1
plt.show()

我們可以看到上圖,所有圖像現在都具有完全相同的大小,這與我用於本帖子封面圖片的圖像不同。

加載測試圖像

我們已經知道所有訓練數據都已成功加載,現在我們可以使用完全相同的函數加載測試數據。步驟幾乎相同,但是這里我將那些加載的數據存儲在X_test和y_test數組中。用於測試的數據本身包含624個樣本。

norm_images_test, norm_labels_test = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/test/NORMAL/')pneu_images_test, pneu_labels_test = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/test/PNEUMONIA/')X_test = np.append(norm_images_test, pneu_images_test, axis=0)
y_test = np.append(norm_labels_test, pneu_labels_test)

此外,我注意到僅加載整個數據集就需要很長時間。因此,我將使用pickle模塊將X_train,X_test,y_train和y_test保存在單獨的文件中。這樣我下次想再使用這些數據的時候,就不需要再次運行這些代碼了。

# Use this to save variables
with open('pneumonia_data.pickle', 'wb') as f:
    pickle.dump((X_train, X_test, y_train, y_test), f)# Use this to load variables
with open('pneumonia_data.pickle', 'rb') as f:
    (X_train, X_test, y_train, y_test) = pickle.load(f)

由於所有X數據都經過了很好的預處理,因此現在使用標簽y_train和y_test了。

標簽預處理

此時,兩個y變量都由以字符串數據類型編寫的正常,細菌或病毒組成。實際上,這樣的標簽只是神經網絡所不能接受的。因此,我們需要將其轉換為單一格式。

幸運的是,我們從Scikit-Learn模塊獲取了 OneHotEncoder對象,它對完成轉換非常有幫助。為此,我們需要先在y_train和y_test上創建一個新軸。(我們創建了這個新軸,因為那是OneHotEncoder期望的形狀)。

y_train = y_train[:, np.newaxis]
y_test = y_test[:, np.newaxis]

接下來,像這樣初始化one_hot_encoder。請注意,在這里我將False作為稀疏參數傳遞,以便簡化下一步。但是,如果你想使用稀疏矩陣,則只需使用sparse = True或將參數保留為空即可。

one_hot_encoder = OneHotEncoder(sparse=False)

最后,我們將使用one_hot_encoder將這些y數據轉換為one-hot。然后將編碼后的標簽存儲在y_train_one_hot和y_test_one_hot中。這兩個數組是我們將用於訓練的標簽。

y_train_one_hot = one_hot_encoder.fit_transform(y_train)
y_test_one_hot = one_hot_encoder.transform(y_test)

將數據X重塑為(None,200,200,1)

現在讓我們回到X_train和X_test。重要的是要知道這兩個數組的形狀分別為(5216、200、200)和(624、200、200)。

乍一看,這兩個形狀看起來還可以,因為我們可以使用plt.imshow()函數進行顯示。但是,這種形狀卷積層不可接受,因為它希望將一個顏色通道作為其輸入。

因此,由於該圖像本質上是灰度圖像,因此我們需要添加一個1維的新軸,該軸將被卷積層識別為唯一的顏色通道。雖然它的實現並不像我的解釋那么復雜:

X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], X_train.shape[2], 1)
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], X_test.shape[2], 1)

運行上述代碼后,如果我們同時檢查X_train和X_test的形狀,那么我們將看到現在的形狀分別是(5216,200,200,1)和(624,200,200,1)。

數據擴充

增加數據(或者更具體地說是增加訓練數據)的要點是,我們將通過創建更多的樣本(每個樣本都具有某種隨機性)來增加用於訓練的數據數量。這些隨機性可能包括平移、旋轉、縮放、剪切和翻轉。

這種技術可以幫助我們的神經網絡分類器減少過擬合,或者說,它可以使模型更好地泛化數據樣本。幸運的是,由於存在可以從Keras模塊導入的ImageDataGenerator對象,實現非常簡單。

datagen = ImageDataGenerator(
        rotation_range = 10,  
        zoom_range = 0.1, 
        width_shift_range = 0.1, 
        height_shift_range = 0.1)

因此,我在上面的代碼中所做的基本上是設置隨機范圍。如果你想了解每個參數的詳細信息,請點擊這里鏈接到ImageDataGenerator的文檔。

接下來,在初始化datagen對象之后,我們需要做的是使它和我們的X_train相匹配。然后,該過程被隨后施加的flow()的方法,該步驟中是非常有用的,使得所述 train_gen對象現在能夠產生增強數據的批次。

datagen.fit(X_train)train_gen = datagen.flow(X_train, y_train_one_hot, batch_size=32)

CNN(卷積神經網絡)

現在是時候真正構建神經網絡架構了。讓我們從輸入層(input1)開始。因此,這一層基本上會獲取X數據中的所有圖像樣本。因此,我們需要確保第一層接受與圖像尺寸完全相同的形狀。值得注意的是,我們僅需要定義(寬度,高度,通道),而不是(樣本,寬度,高度,通道)。

此后,此輸入層連接到幾對卷積池層對,然后最終連接到全連接層。請注意,由於ReLU的計算速度比S型更快,因此模型中的所有隱藏層都使用ReLU激活函數,因此所需的訓練時間更短。最后,要連接的最后一層是output1,它由3個具有softmax激活函數的神經元組成。

這里使用softmax是因為我們希望輸出是每個類別的概率值。

input1 = Input(shape=(X_train.shape[1], X_train.shape[2], 1))

cnn = Conv2D(16, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(input1)
cnn = Conv2D(32, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Conv2D(16, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = Conv2D(32, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Flatten()(cnn)
cnn = Dense(100, activation='relu')(cnn)
cnn = Dense(50, activation='relu')(cnn)
output1 = Dense(3, activation='softmax')(cnn)

model = Model(inputs=input1, outputs=output1)

在使用上面的代碼構造了神經網絡之后,我們可以通過對model對象應用summary()來顯示模型的摘要。下面是我們的CNN模型的詳細情況。我們可以看到我們總共有800萬個參數——這確實很多。好吧,這就是為什么我在Kaggle Notebook上運行這個代碼。

總之,在構建模型之后,我們需要使用分類交叉熵損失函數和Adam優化器來編譯神經網絡。使用這個損失函數,因為它只是多類分類任務中常用的函數。同時,我選擇Adam作為優化器,因為它是在大多數神經網絡任務中最小化損失的最佳選擇。

model.compile(loss='categorical_crossentropy', 
              optimizer='adam', metrics=['acc'])

現在是時候訓練模型了!在這里,我們將使用fit_generator()而不是fit(),因為我們將從train_gen對象獲取訓練數據。如果你關注數據擴充部分,你會注意到train_gen是使用X_train和y_train_one_hot創建的。因此,我們不需要在fit_generator()方法中顯式定義X-y對。

history = model.fit_generator(train_gen, epochs=30, 
          validation_data=(X_test, y_test_one_hot))

train_gen的特殊之處在於,訓練過程中將使用具有一定隨機性的樣本來完成。因此,我們在X_train中擁有的所有訓練數據都不會直接輸入到神經網絡中。取而代之的是,這些樣本將被用作生成器的基礎,通過一些隨機變換生成一個新圖像。

此外,該生成器在每個時期產生不同的圖像,這對於我們的神經網絡分類器更好地泛化測試集中的樣本非常有利。下面是訓練的過程。

Epoch 1/30
163/163 [==============================] - 19s 114ms/step - loss: 5.7014 - acc: 0.6133 - val_loss: 0.7971 - val_acc: 0.7228
.
.
.
Epoch 10/30
163/163 [==============================] - 18s 111ms/step - loss: 0.5575 - acc: 0.7650 - val_loss: 0.8788 - val_acc: 0.7308
.
.
.
Epoch 20/30
163/163 [==============================] - 17s 102ms/step - loss: 0.5267 - acc: 0.7784 - val_loss: 0.6668 - val_acc: 0.7917
.
.
.
Epoch 30/30
163/163 [==============================] - 17s 104ms/step - loss: 0.4915 - acc: 0.7922 - val_loss: 0.7079 - val_acc: 0.8045

整個訓練本身在我的Kaggle Notebook上花費了大約10分鍾。所以要耐心點!經過訓練后,我們可以繪制出准確度得分的提高和損失值的降低,如下所示:

plt.figure(figsize=(8,6))
plt.title('Accuracy scores')
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.legend(['acc', 'val_acc'])
plt.show()plt.figure(figsize=(8,6))
plt.title('Loss value')
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.show()

根據上面的兩個圖,我們可以說,即使在這30個時期內測試准確性和損失值都在波動,模型的性能仍在不斷提高。

這里要注意的另一重要事情是,由於我們在項目的早期應用了數據增強方法,因此該模型不會遭受過擬合的困擾。我們在這里可以看到,在最終迭代中,訓練和測試數據的准確性分別為79%和80%。

有趣的事實:在實施數據增強方法之前,我在訓練數據上獲得了100%的准確性,在測試數據上獲得了64%的准確性,這顯然是過擬合了。因此,我們可以在此處清楚地看到,增加訓練數據對於提高測試准確性得分非常有效,同時也可以減少過擬合。

模型評估

現在,讓我們深入了解使用混淆矩陣得出的測試數據的准確性。首先,我們需要預測所有X_test並將結果從獨熱格式轉換回其實際的分類標簽。

predictions = model.predict(X_test)
predictions = one_hot_encoder.inverse_transform(predictions)

接下來,我們可以像這樣使用confusion_matrix()函數:

cm = confusion_matrix(y_test, predictions)

重要的是要注意函數中使用的參數是(實際值,預測值)。該混淆矩陣函數的返回值是一個二維數組,用於存儲預測分布。為了使矩陣更易於解釋,我們可以使用Seaborn模塊中的heatmap()函數進行顯示。順便說一句,這里的類名列表的值是根據one_hot_encoder.categories_返回的順序獲取的。

classnames = ['bacteria', 'normal', 'virus']plt.figure(figsize=(8,8))
plt.title('Confusion matrix')
sns.heatmap(cm, cbar=False, xticklabels=classnames, yticklabels=classnames, fmt='d', annot=True, cmap=plt.cm.Blues)
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.show()

根據上面的混淆矩陣,我們可以看到45張病毒X射線圖像被預測為細菌。這可能是因為很難區分這兩種肺炎。但是,至少因為我們對242個樣本中的232個進行了正確分類,所以我們的模型至少能夠很好地預測由細菌引起的肺炎。

這就是整個項目!謝謝閱讀!下面是運行整個項目所需的所有代碼。

import os
import cv2
import pickle	# Used to save variables
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm	# Used to display progress bar
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import confusion_matrix
from keras.models import Model, load_model
from keras.layers import Dense, Input, Conv2D, MaxPool2D, Flatten
from keras.preprocessing.image import ImageDataGenerator	# Used to generate images

np.random.seed(22)

# Do not forget to include the last slash
def load_normal(norm_path):
    norm_files = np.array(os.listdir(norm_path))
    norm_labels = np.array(['normal']*len(norm_files))
    
    norm_images = []
    for image in tqdm(norm_files):
		# Read image
        image = cv2.imread(norm_path + image)
		# Resize image to 200x200 px
        image = cv2.resize(image, dsize=(200,200))
		# Convert to grayscale
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        norm_images.append(image)
        
    norm_images = np.array(norm_images)
    
    return norm_images, norm_labels

def load_pneumonia(pneu_path):
    pneu_files = np.array(os.listdir(pneu_path))
    pneu_labels = np.array([pneu_file.split('_')[1] for pneu_file in pneu_files])
    
    pneu_images = []
    for image in tqdm(pneu_files):
		# Read image
        image = cv2.imread(pneu_path + image)
		# Resize image to 200x200 px
        image = cv2.resize(image, dsize=(200,200))
		# Convert to grayscale
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        pneu_images.append(image)
        
    pneu_images = np.array(pneu_images)
    
    return pneu_images, pneu_labels


print('Loading images')
# All images are stored in _images, all labels are in _labels
norm_images, norm_labels = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/train/NORMAL/')
pneu_images, pneu_labels = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/train/PNEUMONIA/')

# Put all train images to X_train 
X_train = np.append(norm_images, pneu_images, axis=0)

# Put all train labels to y_train
y_train = np.append(norm_labels, pneu_labels)

print(X_train.shape)
print(y_train.shape)
# Finding out the number of samples of each class
print(np.unique(y_train, return_counts=True))

print('Display several images')
fig, axes = plt.subplots(ncols=7, nrows=2, figsize=(16, 4))

indices = np.random.choice(len(X_train), 14)
counter = 0

for i in range(2):
    for j in range(7):
        axes[i,j].set_title(y_train[indices[counter]])
        axes[i,j].imshow(X_train[indices[counter]], cmap='gray')
        axes[i,j].get_xaxis().set_visible(False)
        axes[i,j].get_yaxis().set_visible(False)
        counter += 1
plt.show()


print('Loading test images')
# Do the exact same thing as what we have done on train data
norm_images_test, norm_labels_test = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/test/NORMAL/')
pneu_images_test, pneu_labels_test = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/test/PNEUMONIA/')
X_test = np.append(norm_images_test, pneu_images_test, axis=0)
y_test = np.append(norm_labels_test, pneu_labels_test)

# Save the loaded images to pickle file for future use
with open('pneumonia_data.pickle', 'wb') as f:
    pickle.dump((X_train, X_test, y_train, y_test), f)

# Here's how to load it
with open('pneumonia_data.pickle', 'rb') as f:
    (X_train, X_test, y_train, y_test) = pickle.load(f)

print('Label preprocessing')

# Create new axis on all y data
y_train = y_train[:, np.newaxis]
y_test = y_test[:, np.newaxis]

# Initialize OneHotEncoder object
one_hot_encoder = OneHotEncoder(sparse=False)

# Convert all labels to one-hot
y_train_one_hot = one_hot_encoder.fit_transform(y_train)
y_test_one_hot = one_hot_encoder.transform(y_test)

print('Reshaping X data')
# Reshape the data into (no of samples, height, width, 1), where 1 represents a single color channel
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], X_train.shape[2], 1)
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], X_test.shape[2], 1)

print('Data augmentation')
# Generate new images with some randomness
datagen = ImageDataGenerator(
		rotation_range = 10,  
        zoom_range = 0.1, 
        width_shift_range = 0.1, 
        height_shift_range = 0.1)

datagen.fit(X_train)
train_gen = datagen.flow(X_train, y_train_one_hot, batch_size = 32)

print('CNN')

# Define the input shape of the neural network
input_shape = (X_train.shape[1], X_train.shape[2], 1)
print(input_shape)

input1 = Input(shape=input_shape)

cnn = Conv2D(16, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(input1)
cnn = Conv2D(32, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Conv2D(16, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = Conv2D(32, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Flatten()(cnn)
cnn = Dense(100, activation='relu')(cnn)
cnn = Dense(50, activation='relu')(cnn)
output1 = Dense(3, activation='softmax')(cnn)

model = Model(inputs=input1, outputs=output1)

model.compile(loss='categorical_crossentropy', 
              optimizer='adam', metrics=['acc'])

# Using fit_generator() instead of fit() because we are going to use data
# taken from the generator. Note that the randomness is changing
# on each epoch
history = model.fit_generator(train_gen, epochs=30, 
          validation_data=(X_test, y_test_one_hot))

# Saving model
model.save('pneumonia_cnn.h5')

print('Displaying accuracy')
plt.figure(figsize=(8,6))
plt.title('Accuracy scores')
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.legend(['acc', 'val_acc'])
plt.show()

print('Displaying loss')
plt.figure(figsize=(8,6))
plt.title('Loss value')
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.show()

# Predicting test data
predictions = model.predict(X_test)
print(predictions)

predictions = one_hot_encoder.inverse_transform(predictions)

print('Model evaluation')
print(one_hot_encoder.categories_)

classnames = ['bacteria', 'normal', 'virus']

# Display confusion matrix
cm = confusion_matrix(y_test, predictions)
plt.figure(figsize=(8,8))
plt.title('Confusion matrix')
sns.heatmap(cm, cbar=False, xticklabels=classnames, yticklabels=classnames, fmt='d', annot=True, cmap=plt.cm.Blues)
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.show()

參考文獻

JędrzejDudzicz對胸部X線檢查的肺炎檢出率約為92%

Kerian ImageDataGenerator和Adrian Rosebrock的數據增強

原文鏈接:https://www.analyticsvidhya.com/blog/2020/09/pneumonia-detection-using-cnn-with-implementation-in-python/

歡迎關注磐創AI博客站:
http://panchuang.net/

sklearn機器學習中文官方文檔:
http://sklearn123.com/

歡迎關注磐創博客資源匯總站:
http://docs.panchuang.net/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM