diff --git a/Model_Approval.ipynb b/Model_Approval.ipynb new file mode 100644 index 0000000..6265dd0 --- /dev/null +++ b/Model_Approval.ipynb @@ -0,0 +1,2328 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "443ec0a7-722d-496c-83a6-1dbb18c9961c", + "metadata": {}, + "source": [ + "# Imports " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ca8782f5-8c0d-4c8a-88d2-d83ada30c1a6", + "metadata": {}, + "outputs": [], + "source": [ + "import glob\n", + "import cv2 as cv\n", + "from sklearn.metrics import accuracy_score, confusion_matrix, precision_score, recall_score, ConfusionMatrixDisplay\n", + "import os\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from heatmap import heatmap, corrplot\n", + "\n", + "from sklearn.model_selection import RandomizedSearchCV, train_test_split\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from sklearn.cluster import KMeans\n", + "import pickle\n", + "\n", + "### PoreAnalyzer Package ################################################################################\n", + "\n", + "from analyzer.classificator import Preprocessing, DimensionReductionPCA, KMeansClassifier, DBSCANClassifier\n", + "from analyzer.features import Pore" + ] + }, + { + "cell_type": "markdown", + "id": "feb34a60-a784-4f0b-b8f5-f2c182b34002", + "metadata": { + "tags": [] + }, + "source": [ + "# Pore seperation and Feature Extraktion" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "257e0668-9382-474f-b341-5a2a04ac0f7d", + "metadata": {}, + "outputs": [], + "source": [ + "# Poren einzeln aus Bildern auslesen und speichern\n", + "images = [filepath for filepath in glob.iglob('Images/Ti6Al4V-Stichprobe_4/Images/*.jpg')]\n", + "process_time = []\n", + "\n", + "# lists for extracted features\n", + "solidity = []\n", + "area = []\n", + "defect_density = []\n", + "perimeter = []\n", + "mean_conv_defect = []\n", + "img_names = []\n", + "pore_number = []\n", + "images_index = []\n", + "pore_index = []\n", + "\n", + "# try: \n", + "# os.makedirs('Images/AlSi35_Analysis/all_pores')\n", + "# except OSError:\n", + "# pass\n", + "\n", + "for i, image in enumerate(images):\n", + " \n", + " # if i == 2:\n", + " # break\n", + " image_saved = False\n", + "\n", + " time_zero = time.time() # Zeit zum Beginn des Loops speichern\n", + " \n", + " img = cv.imread(image, cv.IMREAD_UNCHANGED) # Bild laden\n", + " head_tail = os.path.split(image)\n", + " names_split = int(os.path.splitext(head_tail[1])[0].removesuffix('_4')) \n", + " \n", + " try: \n", + " specimen=Micrograph(img, scale=1.79, cropsize_microns=2000)\n", + " except:\n", + " continue\n", + " \n", + " for j, pore in enumerate(specimen.prs):\n", + " pore_sep = PoreSeperator(pore.contour, specimen.img_cnt, segmentsize=0.1)\n", + " pore_index.append(str(i) + '_' + str(j))\n", + " if pore_sep.check_size() == True:\n", + " pore_sep.save('Images/Ti6Al4V-Stichprobe_4/pores/{}_{}.jpg'.format(names_split, j))#i,j))\n", + " image_saved = True\n", + " \n", + " # save features of saved pore in list\n", + " solidity.append(pore.solidity)\n", + " area.append(pore.area)\n", + " defect_density.append(pore.defect_density)\n", + " perimeter.append(pore.perimeter)\n", + " mean_conv_defect.append(pore.mean_defect)\n", + " img_names.append(names_split)\n", + " pore_number.append(j)\n", + " images_index.append(str(i) + '_' + str(j))\n", + " \n", + " process_time.append(time.time()-time_zero)\n", + " time_pending = (len(images)-i-1)*sum(process_time)/len(process_time)\n", + " pending_hours = int(time_pending*0.01666666/60)\n", + " pending_minutes = int(time_pending*0.01666666-(60*pending_hours))\n", + " print('\\r Image {:03d} of {}, time pending: {:02d} h {:02d} Min'.format(i+1, len(images), pending_hours, pending_minutes), end=\"\") " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2dd5fd5-731b-413b-82b7-71a501ac3ef5", + "metadata": {}, + "outputs": [], + "source": [ + "dataframe = {'img': img_names, 'pore_number': pore_number, 'image_index': images_index,'solidity': solidity, 'area': area, 'defect_density': defect_density, 'perimeter': perimeter, 'mean_defect': mean_conv_defect}\n", + "\n", + "dataframe = pd.DataFrame.from_dict(dataframe)\n", + "dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f4da05d-06b1-4c03-b57d-061fa99adcd4", + "metadata": {}, + "outputs": [], + "source": [ + "dataframe.to_csv('Images/AlSi10Mg_Pores/AlSi10Mg_Pore_Features.csv', sep=';', decimal=\",\", index=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d8155d5c-52b1-4407-8f3f-28bb1d4a38b7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
imgpore_numberimage_indexsolidityareadefect_densityperimetermean_defect
01100_101.0000002.6477310.0000006.1789820.000000
11110_110.96703313.70590014.59225614.5904430.301292
21120_120.967553487.6496772.46078387.1363940.506548
31130_130.96153870.0869878.56079032.5296060.340241
41140_140.974747420.8334174.27722778.0710540.381112
...........................
9006198657634_6570.788834374.1087591.871114102.7869822.095467
9006298658634_6581.0000000.6229950.0000003.1572020.000000
9006398659634_6590.9655174.36096822.9306898.0845220.249599
9006498660634_6600.89873411.05816936.17235415.8421060.290443
9006598666634_6660.901754120.0823708.32761747.7178330.590924
\n", + "

90066 rows × 8 columns

\n", + "
" + ], + "text/plain": [ + " img pore_number image_index solidity area defect_density \\\n", + "0 1 10 0_10 1.000000 2.647731 0.000000 \n", + "1 1 11 0_11 0.967033 13.705900 14.592256 \n", + "2 1 12 0_12 0.967553 487.649677 2.460783 \n", + "3 1 13 0_13 0.961538 70.086987 8.560790 \n", + "4 1 14 0_14 0.974747 420.833417 4.277227 \n", + "... ... ... ... ... ... ... \n", + "90061 98 657 634_657 0.788834 374.108759 1.871114 \n", + "90062 98 658 634_658 1.000000 0.622995 0.000000 \n", + "90063 98 659 634_659 0.965517 4.360968 22.930689 \n", + "90064 98 660 634_660 0.898734 11.058169 36.172354 \n", + "90065 98 666 634_666 0.901754 120.082370 8.327617 \n", + "\n", + " perimeter mean_defect \n", + "0 6.178982 0.000000 \n", + "1 14.590443 0.301292 \n", + "2 87.136394 0.506548 \n", + "3 32.529606 0.340241 \n", + "4 78.071054 0.381112 \n", + "... ... ... \n", + "90061 102.786982 2.095467 \n", + "90062 3.157202 0.000000 \n", + "90063 8.084522 0.249599 \n", + "90064 15.842106 0.290443 \n", + "90065 47.717833 0.590924 \n", + "\n", + "[90066 rows x 8 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dataframe = pd.read_csv('Images/Ti6Al4V_Analysis/Ti6Al4V_Pore_Features.csv', sep=';', decimal=\",\")\n", + "dataframe" + ] + }, + { + "cell_type": "markdown", + "id": "7838f6b5-6e2e-4c27-bea8-82a5202a3348", + "metadata": { + "tags": [] + }, + "source": [ + "# Import Labeled Data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7f652c2d-79d0-4206-a64a-2251a0c767d3", + "metadata": {}, + "outputs": [], + "source": [ + "# loading image names for the pores in the label folders\n", + "images_kh = [filepath for filepath in glob.iglob('Images\\Ti6Al4V_Analysis\\labeled\\Keyhole\\*.jpg')]\n", + "images_lof = [filepath for filepath in glob.iglob('Images\\Ti6Al4V_Analysis\\labeled\\Lack_of_Fusion\\*.jpg')]\n", + "images_process = [filepath for filepath in glob.iglob('Images\\Ti6Al4V_Analysis\\labeled\\Process\\*.jpg')]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b7e21dde-ff64-48de-b661-b86e91bea65e", + "metadata": {}, + "outputs": [], + "source": [ + "# loading images and label number \n", + "img = []\n", + "labels = []\n", + "for image in images_kh:\n", + " img.append(cv.resize(cv.imread(image, cv.IMREAD_GRAYSCALE), dsize=(100, 100), interpolation=cv.INTER_CUBIC))\n", + " # img.append(cv.imread(image, cv.IMREAD_GRAYSCALE))\n", + " labels.append(0)\n", + " \n", + "for image in images_lof:\n", + " img.append(cv.resize(cv.imread(image, cv.IMREAD_GRAYSCALE), dsize=(100, 100), interpolation=cv.INTER_CUBIC))\n", + " # img.append(cv.imread(image, cv.IMREAD_GRAYSCALE))\n", + " labels.append(1)\n", + " \n", + "for image in images_process:\n", + " img.append(cv.resize(cv.imread(image, cv.IMREAD_GRAYSCALE), dsize=(100, 100), interpolation=cv.INTER_CUBIC))\n", + " # img.append(cv.imread(image, cv.IMREAD_GRAYSCALE))\n", + " labels.append(2)\n", + " \n", + "images = images_kh + images_lof + images_process" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "75beeebc-f8ef-456a-b320-4d9e66dba0ad", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1200" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(img)" + ] + }, + { + "cell_type": "markdown", + "id": "5f0cbcdc-6e38-4800-81ab-011b9aad9cf6", + "metadata": { + "tags": [] + }, + "source": [ + "## Pixelfeatures" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2c373b22-2f10-4eb0-845e-136e81b86bbd", + "metadata": {}, + "outputs": [], + "source": [ + "# flattening images into one long vector\n", + "data_px = Preprocessing(img)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0668b012-ed59-4d96-9e56-733c3b99c0c9", + "metadata": {}, + "outputs": [], + "source": [ + "data_px.dataframe.to_csv('Images/Ti6Al4V_Analysis/Ti6Al4V_Pore_PX_flattened.csv', sep=';', decimal=\",\", index=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "86333f14-8bef-4497-8ddc-5b63480ccec8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Varianz explained by PCA model with 250 Components is 96.888 %.\n" + ] + } + ], + "source": [ + "# linear dimension reduction to 250 principle components\n", + "data_red = DimensionReductionPCA(data_px.dataframe, k=250)\n", + "data_red.pca_explain()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5f12a260-53aa-4448-b611-42e21daed87a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PCA Model saved as Models/PCA_Model_Binary_RF.pickle\n" + ] + } + ], + "source": [ + "data_red.save_pca_model(name='Models/PCA_Model_Binary_RF')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5d03273-0888-4267-bd84-79456a713127", + "metadata": {}, + "outputs": [], + "source": [ + "# scaling data if wanted\n", + "data_red = data_red.dataframe #.scale()" + ] + }, + { + "cell_type": "markdown", + "id": "b8a2e6e9-2288-4106-a0a7-82c208dc957d", + "metadata": { + "tags": [] + }, + "source": [ + "## Local Features" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "65c40857-6671-40b3-88c6-a50bef1d840e", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\altmann\\AppData\\Local\\Temp\\ipykernel_7948\\4138243829.py:21: SettingWithCopyWarning: \n", + "A value is trying to be set on a copy of a slice from a DataFrame.\n", + "Try using .loc[row_indexer,col_indexer] = value instead\n", + "\n", + "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", + " local_features['label'] = labels\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
imgpore_numberimage_indexsolidityareadefect_densityperimetermean_defectlabel
01100_101.0000002.6477310.0000006.1789820.0000000
11110_110.96703313.70590014.59225614.5904430.3012920
51150_150.9830102469.0866611.579531192.7304050.4486640
61170_170.96402920.87034719.16594917.2852840.2754460
101220_221.0000005.1397120.0000008.4114610.0000000
..............................
9005598651634_6510.8484854.36096845.8613779.2007620.3795392
9006198657634_6570.788834374.1087591.871114102.7869822.0954672
9006398659634_6590.9655174.36096822.9306898.0845220.2495992
9006498660634_6600.89873411.05816936.17235415.8421060.2904432
9006598666634_6660.901754120.0823708.32761747.7178330.5909242
\n", + "

1200 rows × 9 columns

\n", + "
" + ], + "text/plain": [ + " img pore_number image_index solidity area defect_density \\\n", + "0 1 10 0_10 1.000000 2.647731 0.000000 \n", + "1 1 11 0_11 0.967033 13.705900 14.592256 \n", + "5 1 15 0_15 0.983010 2469.086661 1.579531 \n", + "6 1 17 0_17 0.964029 20.870347 19.165949 \n", + "10 1 22 0_22 1.000000 5.139712 0.000000 \n", + "... ... ... ... ... ... ... \n", + "90055 98 651 634_651 0.848485 4.360968 45.861377 \n", + "90061 98 657 634_657 0.788834 374.108759 1.871114 \n", + "90063 98 659 634_659 0.965517 4.360968 22.930689 \n", + "90064 98 660 634_660 0.898734 11.058169 36.172354 \n", + "90065 98 666 634_666 0.901754 120.082370 8.327617 \n", + "\n", + " perimeter mean_defect label \n", + "0 6.178982 0.000000 0 \n", + "1 14.590443 0.301292 0 \n", + "5 192.730405 0.448664 0 \n", + "6 17.285284 0.275446 0 \n", + "10 8.411461 0.000000 0 \n", + "... ... ... ... \n", + "90055 9.200762 0.379539 2 \n", + "90061 102.786982 2.095467 2 \n", + "90063 8.084522 0.249599 2 \n", + "90064 15.842106 0.290443 2 \n", + "90065 47.717833 0.590924 2 \n", + "\n", + "[1200 rows x 9 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# all features are contained in dataframe --> extracted features of all pores in dataset \n", + "\n", + "# slicing name of image\n", + "names_kh = [os.path.splitext(os.path.split(image)[1])[0] for image in images_kh]\n", + "names_lof = [os.path.splitext(os.path.split(image)[1])[0] for image in images_lof]\n", + "names_process = [os.path.splitext(os.path.split(image)[1])[0] for image in images_process]\n", + "\n", + "# combining lists\n", + "labeled_pores = names_kh + names_lof + names_process\n", + "\n", + "# selecting rows by combined list\n", + "local_features = dataframe[dataframe['image_index'].isin(labeled_pores)]\n", + "\n", + "# adding label to dataframe\n", + "key_hole = [0 for i in range(len(names_kh))]\n", + "lof = [1 for i in range(len(names_lof))]\n", + "process = [2 for i in range(len(names_process))]\n", + "\n", + "labels = key_hole + lof + process\n", + " \n", + "local_features['label'] = labels\n", + "\n", + "local_features\n", + "\n", + "# local_features.to_csv('Images/Ti6Al4V_Analysis/Ti6Al4V_local_defect_features.csv', sep=';', decimal=\",\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "5e8fc9f5-65f1-4e71-9340-21c75d002d90", + "metadata": { + "tags": [] + }, + "source": [ + "# Unsupervised Models" + ] + }, + { + "cell_type": "markdown", + "id": "e3b83ea5-e8f1-4afd-8193-a2527b2282e4", + "metadata": { + "tags": [] + }, + "source": [ + "## kMeans" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c31d8534-843c-42c2-aa34-c46d63fcbb6f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
pca0pca1pca2pca3pca4pca5pca6pca7pca8pca9...pca241pca242pca243pca244pca245pca246pca247pca248pca249labels
0255.425598-1785.702271925.004456-647.859558558.293701-352.141418-539.285645-457.65905881.950119-70.469582...-14.26652347.72750124.730694-25.18042940.02402516.89291853.906643-17.68145430.3394760
1803.354980-2022.260864837.281555433.116974-408.251678193.452789582.161194264.769287-683.036194174.103912...-2.07882135.56679515.871997-34.090492-36.830696-15.575199-51.617443-81.089500-49.1718600
22319.719727-2092.848877-420.375305-251.272079512.9344481146.280273-162.358948-497.942871-366.30847247.767113...-57.140324-55.58275657.23656590.583664-1.16124614.53749742.300175-20.864361-2.1664840
3-1407.126343-684.353210968.24578923.67079932.504410-1071.493164143.97363379.708427646.462952-257.145782...64.606071-6.576953-96.6572803.912672-26.86332326.66540346.656254-2.38064248.2282520
4-963.219238-1100.2916261195.094727-167.95707782.295097-1099.581055-66.005585140.388641540.216675-121.797462...25.663364-57.5227285.5483590.37056427.228884-36.064571-23.228437-37.61468167.6368940
..................................................................
1195-2778.831543924.899963-434.967377-16.103178-23.349348242.882431-37.833126-41.775177-119.16433743.505074...11.097609-11.073200-13.2535965.406798-17.156864-7.780338-24.389185-10.724975-5.4437702
1196-2698.895264819.713989-325.541809-12.013907-24.603376109.989647-37.048916-21.737972-2.6220026.201177...-13.437889-0.313441-10.38170119.76334425.998217-5.2598573.201698-32.150646-23.8191432
1197-2780.819824928.235535-437.094635-16.398756-21.832441247.764145-35.915752-42.458527-121.82815643.289146...16.934982-24.733269-19.3612796.819247-23.084692-11.6255701.362972-16.261547-4.3186792
1198-2794.181641948.835876-456.649323-16.779737-22.549208278.220184-31.817270-44.791748-148.21752950.467155...-2.029772-4.7046189.0330995.793400-10.758887-7.3317451.210876-14.464475-0.8578572
1199-2736.962158871.259827-377.366730-14.921387-28.683245177.661697-41.354248-23.649105-61.64075128.548815...-6.47954839.46415714.158841-25.68635929.01289739.76305416.16370017.22655111.3078342
\n", + "

1200 rows × 251 columns

\n", + "
" + ], + "text/plain": [ + " pca0 pca1 pca2 pca3 pca4 \\\n", + "0 255.425598 -1785.702271 925.004456 -647.859558 558.293701 \n", + "1 803.354980 -2022.260864 837.281555 433.116974 -408.251678 \n", + "2 2319.719727 -2092.848877 -420.375305 -251.272079 512.934448 \n", + "3 -1407.126343 -684.353210 968.245789 23.670799 32.504410 \n", + "4 -963.219238 -1100.291626 1195.094727 -167.957077 82.295097 \n", + "... ... ... ... ... ... \n", + "1195 -2778.831543 924.899963 -434.967377 -16.103178 -23.349348 \n", + "1196 -2698.895264 819.713989 -325.541809 -12.013907 -24.603376 \n", + "1197 -2780.819824 928.235535 -437.094635 -16.398756 -21.832441 \n", + "1198 -2794.181641 948.835876 -456.649323 -16.779737 -22.549208 \n", + "1199 -2736.962158 871.259827 -377.366730 -14.921387 -28.683245 \n", + "\n", + " pca5 pca6 pca7 pca8 pca9 ... \\\n", + "0 -352.141418 -539.285645 -457.659058 81.950119 -70.469582 ... \n", + "1 193.452789 582.161194 264.769287 -683.036194 174.103912 ... \n", + "2 1146.280273 -162.358948 -497.942871 -366.308472 47.767113 ... \n", + "3 -1071.493164 143.973633 79.708427 646.462952 -257.145782 ... \n", + "4 -1099.581055 -66.005585 140.388641 540.216675 -121.797462 ... \n", + "... ... ... ... ... ... ... \n", + "1195 242.882431 -37.833126 -41.775177 -119.164337 43.505074 ... \n", + "1196 109.989647 -37.048916 -21.737972 -2.622002 6.201177 ... \n", + "1197 247.764145 -35.915752 -42.458527 -121.828156 43.289146 ... \n", + "1198 278.220184 -31.817270 -44.791748 -148.217529 50.467155 ... \n", + "1199 177.661697 -41.354248 -23.649105 -61.640751 28.548815 ... \n", + "\n", + " pca241 pca242 pca243 pca244 pca245 pca246 \\\n", + "0 -14.266523 47.727501 24.730694 -25.180429 40.024025 16.892918 \n", + "1 -2.078821 35.566795 15.871997 -34.090492 -36.830696 -15.575199 \n", + "2 -57.140324 -55.582756 57.236565 90.583664 -1.161246 14.537497 \n", + "3 64.606071 -6.576953 -96.657280 3.912672 -26.863323 26.665403 \n", + "4 25.663364 -57.522728 5.548359 0.370564 27.228884 -36.064571 \n", + "... ... ... ... ... ... ... \n", + "1195 11.097609 -11.073200 -13.253596 5.406798 -17.156864 -7.780338 \n", + "1196 -13.437889 -0.313441 -10.381701 19.763344 25.998217 -5.259857 \n", + "1197 16.934982 -24.733269 -19.361279 6.819247 -23.084692 -11.625570 \n", + "1198 -2.029772 -4.704618 9.033099 5.793400 -10.758887 -7.331745 \n", + "1199 -6.479548 39.464157 14.158841 -25.686359 29.012897 39.763054 \n", + "\n", + " pca247 pca248 pca249 labels \n", + "0 53.906643 -17.681454 30.339476 0 \n", + "1 -51.617443 -81.089500 -49.171860 0 \n", + "2 42.300175 -20.864361 -2.166484 0 \n", + "3 46.656254 -2.380642 48.228252 0 \n", + "4 -23.228437 -37.614681 67.636894 0 \n", + "... ... ... ... ... \n", + "1195 -24.389185 -10.724975 -5.443770 2 \n", + "1196 3.201698 -32.150646 -23.819143 2 \n", + "1197 1.362972 -16.261547 -4.318679 2 \n", + "1198 1.210876 -14.464475 -0.857857 2 \n", + "1199 16.163700 17.226551 11.307834 2 \n", + "\n", + "[1200 rows x 251 columns]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data_red_labeled = data_red.dataframe\n", + "data_red_labeled['labels'] = labels\n", + "data_red_labeled" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2dd84860-c4cd-4296-92ea-86a5b7933344", + "metadata": {}, + "outputs": [], + "source": [ + "X = data_red_labeled.drop(['labels'], axis=1)\n", + "Y = data_red_labeled['labels']" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "417e12ae-63b1-455b-ab06-b30ad4e2e8ac", + "metadata": {}, + "outputs": [], + "source": [ + "# splitting data into training and test partition\n", + "X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.3)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "8e82a915-c9c5-4652-a5af-006c2278a7aa", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\altmann\\Miniconda3\\envs\\poreClustering\\lib\\site-packages\\sklearn\\cluster\\_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning\n", + " warnings.warn(\n", + "C:\\Users\\altmann\\Miniconda3\\envs\\poreClustering\\lib\\site-packages\\sklearn\\cluster\\_kmeans.py:1382: UserWarning: KMeans is known to have a memory leak on Windows with MKL, when there are less chunks than available threads. You can avoid it by setting the environment variable OMP_NUM_THREADS=4.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "
KMeans(n_clusters=3, random_state=42)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "KMeans(n_clusters=3, random_state=42)" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "k = 3\n", + "kmeans = KMeans(n_clusters=k, random_state=42)\n", + "kmeans.fit(X_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "25b5bcb0-0278-4d47-8dc0-8a9e13c1dd68", + "metadata": {}, + "outputs": [], + "source": [ + "labels_pred_kmeans = kmeans.predict(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e3732e70-8db5-411c-935f-c9166ff6eaf2", + "metadata": {}, + "outputs": [], + "source": [ + "modelkmeans = KMeansClassifier(X_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e69cc6d8-e7e6-4620-a0ee-9613d285b94d", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\altmann\\Miniconda3\\envs\\poreClustering\\lib\\site-packages\\sklearn\\cluster\\_kmeans.py:1382: UserWarning: KMeans is known to have a memory leak on Windows with MKL, when there are less chunks than available threads. You can avoid it by setting the environment variable OMP_NUM_THREADS=4.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "modelkmeans.train(k=3)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c8737efd-2503-4461-9802-f361398c042d", + "metadata": {}, + "outputs": [], + "source": [ + "labels_pred_kmeans = modelkmeans.predict(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "05e292e9-41c0-4ae9-8ca5-e830f323d8e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "360" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(labels_pred_kmeans)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "9dd850b2-1451-41c8-b174-bdefb91d2b02", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "360" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(X_test)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "470f14bd-d417-4c22-a7d9-77a79972a909", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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XH369NGiRYv8HQp8LDc3V9OmTdPGjRuVk5Ojuro6DR8+XOXl5f4ODSbCpXcBZODAgbroootcHo3YvXt3jRkzRpmZmX6MDL5ks9m0cuVKjRkzxt+hoBkcPXpU8fHxys3N1RVXXOHvcGASVPYB4vSjEocPH+6y3x+PSgTgOydPnpQkxcXF+TkSmAnJPkC0pEclAvANwzA0c+ZMXXbZZerZs6e/w4GJcLvcANMSHpUIwDfuuusuffnll1q3bp2/Q4HJkOwDREt6VCIA75s+fbreeustrV271tKP7oZv0MYPEC3pUYkAvMcwDN11111644039OGHHyolJcXfIcGEqOwDSEt5VCJ8r6ysTHv37nW+zs/P17Zt2xQXF6eOHTv6MTJ427Rp07RixQq9+eabio6OdnbvYmNjFRER4efoYBZcehdgFi9erPnz5zsflfj0009zeY4Jffzxx7ryyivr7Z8wYYKWL1/e/AHBZ8605mbZsmW6/fbbmzcYmBbJHgAAk2POHgAAkyPZAwBgciR7AABMjmQPAIDJkewBADA5kj0AACZHsgcAwORI9gAAmBzJHvBQenq6LrzwQufr22+/XWPGjGn2OPbv3y+bzaZt27adcUznzp2VlZXV6Pdcvny5Wrdu7XFsNptNq1at8vh9ADQNyR6mdPvtt8tms8lmsykkJERdunTRvffeq/Lycp9/9p/+9KdG39K2MQkaADzFg3BgWtdee62WLVum2tpaffLJJ5o0aZLKy8u1ZMmSemNra2sVEhLilc+NjY31yvsAgLdQ2cO0wsLClJiYqOTkZI0fP1633HKLs5V8uvX+17/+VV26dFFYWJgMw9DJkyf1u9/9TvHx8YqJidFVV12lL774wuV9n3zySSUkJCg6OloTJ05UVVWVy/Eft/EdDofmzZunc889V2FhYerYsaPmzp0rSc7Hmfbt21c2m01Dhw51nrds2TJ1795d4eHhOv/887V48WKXz/n888/Vt29fhYeHq3///tq6davb/0YLFixQr169FBUVpeTkZE2dOlVlZWX1xq1atUrnnXeewsPDNWzYMBUUFLgc/+c//6l+/fopPDxcXbp00aOPPqq6ujq34wHgGyR7WEZERIRqa2udr/fu3avXXntN//jHP5xt9Ouvv16FhYV65513lJeXp4suukhXX321Tpw4IUl67bXXNGfOHM2dO1ebN29Whw4d6iXhH3vwwQc1b948Pfzww9q5c6dWrFihhIQESacStiS9//77OnLkiN544w1J0vPPP6/Zs2dr7ty52rVrlzIyMvTwww8rOztbklReXq5Ro0apW7duysvLU3p6uu699163/01atWqlZ555Rl999ZWys7P14YcfatasWS5jKioqNHfuXGVnZ+vTTz9VSUmJxo0b5zz+3nvv6dZbb9WMGTO0c+dOLV26VMuXL3f+QQOgBTAAE5owYYIxevRo5+vPPvvMaNu2rXHTTTcZhmEYc+bMMUJCQoyioiLnmA8++MCIiYkxqqqqXN6ra9euxtKlSw3DMIxBgwYZU6ZMcTk+cOBAo0+fPg1+dklJiREWFmY8//zzDcaZn59vSDK2bt3qsj85OdlYsWKFy77HH3/cGDRokGEYhrF06VIjLi7OKC8vdx5fsmRJg+/13zp16mQ8/fTTZzz+2muvGW3btnW+XrZsmSHJ2Lhxo3Pfrl27DEnGZ599ZhiGYVx++eVGRkaGy/u8+OKLRocOHZyvJRkrV6484+cC8C3m7GFab7/9ts466yzV1dWptrZWo0eP1sKFC53HO3XqpPbt2ztf5+XlqaysTG3btnV5n8rKSn399deSpF27dmnKlCkuxwcNGqSPPvqowRh27dql6upqXX311Y2O++jRoyooKNDEiRN15513OvfX1dU51wPs2rVLffr0UWRkpEsc7vroo4+UkZGhnTt3qqSkRHV1daqqqlJ5ebmioqIkScHBwerfv7/znPPPP1+tW7fWrl27dPHFFysvL0+bNm1yqeTtdruqqqpUUVHhEiMA/yDZw7SuvPJKLVmyRCEhIUpKSqq3AO90MjvN4XCoQ4cO+vjjj+u9V1MvP4uIiHD7HIfDIelUK3/gwIEux4KCgiRJhmE0KZ7/duDAAV133XWaMmWKHn/8ccXFxWndunWaOHGiy3SHdOrSuR87vc/hcOjRRx/V2LFj640JDw/3OE4AniPZw7SioqJ07rnnNnr8RRddpMLCQgUHB6tz584Njunevbs2btyo2267zblv48aNZ3zP1NRURURE6IMPPtCkSZPqHQ8NDZV0qhI+LSEhQWeffbb27dunW265pcH3veCCC/Tiiy+qsrLS+QfFT8XRkM2bN6uurk5//OMf1arVqeU7r732Wr1xdXV12rx5sy6++GJJ0u7du/Xdd9/p/PPPl3Tq32337t1u/VsDaF4ke+B711xzjQYNGqQxY8Zo3rx56tatmw4fPqx33nlHY8aMUf/+/XX33XdrwoQJ6t+/vy677DK9/PLL2rFjh7p06dLge4aHh+v+++/XrFmzFBoaqksvvVRHjx7Vjh07NHHiRMXHxysiIkKrV6/WOeeco/DwcMXGxio9PV0zZsxQTEyMRo4cqerqam3evFnFxcWaOXOmxo8fr9mzZ2vixIl66KGHtH//fv3v//6vWz9v165dVVdXp4ULF+qGG27Qp59+qmeffbbeuJCQEE2fPl3PPPOMQkJCdNddd+mSSy5xJv9HHnlEo0aNUnJysv7nf/5HrVq10pdffqnt27friSeecP+LAOB1rMYHvmez2fTOO+/oiiuu0G9/+1udd955GjdunPbv3+9cPX/zzTfrkUce0f33369+/frpwIED+v3vf/+T7/vwww/rnnvu0SOPPKLu3bvr5ptvVlFRkaRT8+HPPPOMli5dqqSkJI0ePVqSNGnSJL3wwgtavny5evXqpSFDhmj58uXOS/XOOuss/fOf/9TOnTvVt29fzZ49W/PmzXPr573wwgu1YMECzZs3Tz179tTLL7+szMzMeuMiIyN1//33a/z48Ro0aJAiIiL0yiuvOI+PGDFCb7/9tnJycjRgwABdcsklWrBggTp16uRWPAB8x2Z4Y/IPAAC0WFT2AACYHMkeAACTI9kDAGByJHsAAEyOZA8AgMmR7AEAMDmSPQAAJkeyBwDA5Ej2AACYHMkeAACTI9kDAGBy/x9/G0PZzRssjAAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cm = confusion_matrix(y_test, labels_pred_kmeans)\n", + "\n", + "ConfusionMatrixDisplay(confusion_matrix=cm).plot();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4599e942-4518-4c74-999f-8eadeb77d37b", + "metadata": {}, + "outputs": [], + "source": [ + "try: \n", + " os.makedirs('Images/Ti6Al4V_Analysis/results/results_kmeans/0')\n", + " os.makedirs('Images/Ti6Al4V_Analysis/results/results_kmeans/1')\n", + " os.makedirs('Images/Ti6Al4V_Analysis/results/results_kmeans/2')\n", + " # os.makedirs('pore_label/3')\n", + "except OSError:\n", + " pass\n", + " \n", + "for i, image in enumerate(images):\n", + " label = labels_pred_kmeans[i]\n", + " img = cv.resize(cv.imread(image), dsize=(100, 100), interpolation=cv.INTER_CUBIC)\n", + " cv.imwrite('Images/Ti6Al4V_Analysis/results/results_kmeans/{}/{}.jpg'.format(label, i), img)" + ] + }, + { + "cell_type": "markdown", + "id": "a5692d17-4ed8-4a77-882c-b3b9a401ad5d", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## DBSCAN" + ] + }, + { + "cell_type": "markdown", + "id": "67373261-03a8-4485-8b39-fb2303947c9f", + "metadata": { + "tags": [] + }, + "source": [ + "### Pixel Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d21315f6-b12d-4f0b-8d87-6e60cbf28ca3", + "metadata": {}, + "outputs": [], + "source": [ + "modeldbscan = DBSCANClassifier(data_red.scale(), n_neighbors=2, min_samples=2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e73e9d6-abcc-4cf4-b3d3-193036afe15b", + "metadata": {}, + "outputs": [], + "source": [ + "modeldbscan.knee" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a094ab5-639d-43b6-a70d-64d2a5634a63", + "metadata": {}, + "outputs": [], + "source": [ + "modeldbscan.n_clusters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72b633d8-83ee-47e7-84cc-8a1b70394714", + "metadata": {}, + "outputs": [], + "source": [ + "labels_pred_dbscan = modeldbscan.labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa1f601b-f45b-42f7-80c3-364cff6808ba", + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(-1, modeldbscan.n_clusters, 1):\n", + " try:\n", + " os.makedirs('Images/Ti6Al4V_Analysis/results/results_dbscan/{}'.format(i))\n", + " except OSError:\n", + " pass\n", + " \n", + "for i, image in enumerate(images):\n", + " label = labels_pred_dbscan[i]\n", + " img = cv.resize(cv.imread(image), dsize=(100, 100), interpolation=cv.INTER_CUBIC)\n", + " cv.imwrite('Images/Ti6Al4V_Analysis/results/results_dbscan/{}/{}.jpg'.format(label, i), img)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cef38634-8f5e-4e82-aa03-fe9437f27a96", + "metadata": {}, + "outputs": [], + "source": [ + "cm = confusion_matrix(results['labels'], labels_pred_dbscan+1)\n", + "\n", + "ConfusionMatrixDisplay(confusion_matrix=cm).plot();" + ] + }, + { + "cell_type": "markdown", + "id": "cf856277-6784-4a5f-bdcb-64199fc8d861", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "### local Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94f7c6a3-cdbb-4bd9-8c28-194e637ea7d5", + "metadata": {}, + "outputs": [], + "source": [ + "modeldbscan_lf = DBSCANClassifier(X)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21654256-660d-4943-9fe3-d7a2d0560735", + "metadata": {}, + "outputs": [], + "source": [ + "modeldbscan_lf.knee" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "963f77c2-5b56-4895-9dcf-62ab15498c89", + "metadata": {}, + "outputs": [], + "source": [ + "modeldbscan_lf.n_clusters" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f06534f8-20b4-453a-b904-6f9525e752a1", + "metadata": {}, + "outputs": [], + "source": [ + "labels_pred_dbscan = modeldbscan_lf.labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "baa95e7e-29e0-4f42-999a-5afe93eb40ab", + "metadata": {}, + "outputs": [], + "source": [ + "cm = confusion_matrix(sampling['label'], labels_pred_dbscan+1)\n", + "\n", + "ConfusionMatrixDisplay(confusion_matrix=cm).plot();" + ] + }, + { + "cell_type": "markdown", + "id": "3d7f53de-6c7c-49e7-9a59-3214b6a22425", + "metadata": { + "tags": [] + }, + "source": [ + "# Supervised Models" + ] + }, + { + "cell_type": "markdown", + "id": "3c8e44a1-a205-449e-90ca-00660c9cdd83", + "metadata": { + "tags": [] + }, + "source": [ + "## Binary Image Features" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "b206a135-e9fc-420a-be52-09eba68cad16", + "metadata": {}, + "outputs": [], + "source": [ + "# mixing data rows --> data shouldnt be sorted for splitting \n", + "data_sample = results.copy().sample(frac=1).reset_index(drop=True)\n", + "X = data_sample.drop(['labels'], axis=1)\n", + "Y = data_sample['labels']" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7ee2b19d-a9f2-4880-855d-5d0a83f0d0d9", + "metadata": {}, + "outputs": [], + "source": [ + "# scaling / normalizing the features\n", + "\n", + "# copy the dataframe\n", + "df_norm = X.copy()\n", + "# apply min-max scaling\n", + "for column in df_norm.columns:\n", + " df_norm[column] = (df_norm[column] - df_norm[column].min()) / (df_norm[column].max() - df_norm[column].min())\n", + "\n", + "X = df_norm" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "4f932be2-c645-4b02-a8b5-36194e6b8eed", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
pca0pca1pca2pca3pca4pca5pca6pca7pca8pca9...pca241pca242pca243pca244pca245pca246pca247pca248pca249labels
794-2790.063965941.363220-450.876312-16.881285-23.757185266.390656-35.812542-43.869370-139.33947850.013527...18.105705-4.2262392.247457-21.6065415.6720590.1130892.93867511.76077411.5568082
190-2674.676758787.409058-292.080841-12.459249-23.54597369.260490-40.746521-12.49001733.843327-2.004482...3.731455-0.126414-19.8279760.3328970.4644692.854188-4.628636-10.954101-17.0883392
10241825.270142933.7229001387.870728589.914185-3328.892822381.851166-1327.042969405.855927653.255737513.292725...158.74577328.83920332.63030678.67158546.21959320.28943351.24433943.788345-39.0640261
519-2783.756348931.952576-441.249786-16.820127-22.943647252.919617-36.649456-42.258301-126.11209146.502876...22.02484115.041027-9.137911-17.1081014.355286-3.861822-2.75220520.779800-5.2262222
707-2585.554199673.569702-177.155655-11.210746-24.361341-63.133881-39.08774614.608357139.730865-38.287655...50.294846-23.780457-8.237935-23.4474608.4936961.24148418.59352316.1757356.3432532
125-968.545288-1103.6451421213.158813-135.15843261.409969-1121.710205-16.381384171.112793562.359192-147.075012...13.5194307.0004457.26580045.118504-5.01408924.601515-31.84525131.63601749.2246280
\n", + "

6 rows × 251 columns

\n", + "
" + ], + "text/plain": [ + " pca0 pca1 pca2 pca3 pca4 \\\n", + "794 -2790.063965 941.363220 -450.876312 -16.881285 -23.757185 \n", + "190 -2674.676758 787.409058 -292.080841 -12.459249 -23.545973 \n", + "1024 1825.270142 933.722900 1387.870728 589.914185 -3328.892822 \n", + "519 -2783.756348 931.952576 -441.249786 -16.820127 -22.943647 \n", + "707 -2585.554199 673.569702 -177.155655 -11.210746 -24.361341 \n", + "125 -968.545288 -1103.645142 1213.158813 -135.158432 61.409969 \n", + "\n", + " pca5 pca6 pca7 pca8 pca9 ... \\\n", + "794 266.390656 -35.812542 -43.869370 -139.339478 50.013527 ... \n", + "190 69.260490 -40.746521 -12.490017 33.843327 -2.004482 ... \n", + "1024 381.851166 -1327.042969 405.855927 653.255737 513.292725 ... \n", + "519 252.919617 -36.649456 -42.258301 -126.112091 46.502876 ... \n", + "707 -63.133881 -39.087746 14.608357 139.730865 -38.287655 ... \n", + "125 -1121.710205 -16.381384 171.112793 562.359192 -147.075012 ... \n", + "\n", + " pca241 pca242 pca243 pca244 pca245 pca246 \\\n", + "794 18.105705 -4.226239 2.247457 -21.606541 5.672059 0.113089 \n", + "190 3.731455 -0.126414 -19.827976 0.332897 0.464469 2.854188 \n", + "1024 158.745773 28.839203 32.630306 78.671585 46.219593 20.289433 \n", + "519 22.024841 15.041027 -9.137911 -17.108101 4.355286 -3.861822 \n", + "707 50.294846 -23.780457 -8.237935 -23.447460 8.493696 1.241484 \n", + "125 13.519430 7.000445 7.265800 45.118504 -5.014089 24.601515 \n", + "\n", + " pca247 pca248 pca249 labels \n", + "794 2.938675 11.760774 11.556808 2 \n", + "190 -4.628636 -10.954101 -17.088339 2 \n", + "1024 51.244339 43.788345 -39.064026 1 \n", + "519 -2.752205 20.779800 -5.226222 2 \n", + "707 18.593523 16.175735 6.343253 2 \n", + "125 -31.845251 31.636017 49.224628 0 \n", + "\n", + "[6 rows x 251 columns]" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sampling = X.copy()\n", + "sampling['labels'] = Y\n", + "sampling.sample(6)" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "8565c9ce-cfd2-455f-b978-9d68fcc5ecd6", + "metadata": {}, + "outputs": [], + "source": [ + "# splitting data into training and test partition\n", + "X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.3)" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "f4f79f8a-10f3-4ec1-a19f-2c5089f595f5", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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qDX3OWLEiHr3WVpaipKSEq0bEZGjMGSoFADs27cPUVFR8PT0RLt27bBq1Sqt148fP4777rsP4eHhUCgUSE1NtWD0dVOXmWINVCIyhUEJalFREVQqFQJqLLAcEBCAQvXSIfVQKpX48ssvMXXqVK3thYWFBu9z8eLF8PX11dxCQkIMOBIiItsxdKhUfn4+7r77bsTGxiI7OxvPPvssZs6cibS0NE2bmzdvol27dnj11VcRaMPsUH3arnFKJyIyiFGTpBQ1itsJIWpt02XdunXw8/PD2LFjTd7n3LlzUVxcrLkVFBToFzwRkY0tWbIEU6ZMwdSpUxEZGYnU1FSEhIRoDZ2qbtWqVQgNDUVqaioiIyMxdepUTJ48GW+88YamTd++ffH666/jgQcegIeHh7UORYsQXOaUiMzDoATV398frq6utXo2L1++XKsHtCYhBNauXYukpCS4u7trvRYYGGjwPj08PODj46N1IyKyd8YMlaprGNShQ4dQXl5udCzmHipVUgLcuiUfsweViExhUILq7u6OqKgo7N69W2v77t27ERMTU+979+3bh9OnT2PKlCm1Xhs4cGCtfe7atavBfRIRORpjhkrVNQyqoqICRUVFRsdi7qFS6vB9fGTBfiIiYxm8klRKSgqSkpIQHR2NgQMH4t1338W5c+cw/X/TNefOnYsLFy5g/fr1Wu9bs2YN+vfvj27dutXa56xZszB48GC89tprGDNmDLZu3Yo9e/bgm2++MfKwiIjsm6HDmnS117XdEHPnzkVKSormeUlJiUlJatu2cpnTP/80ehdERACMSFATExNx5coVLFy4EEqlEt26dUNGRoZmVr5Sqaw10L+4uBhpaWlYunSpzn3GxMRg48aNeO655/D888+jffv22LRpE/r372/EIRER2S9jhkrVNQyqSZMmaNmypdGxeHh4mHW8arNmXOaUiMzD4AQVAGbMmIEZM2bofG3dunW1tvn6+uLmzZv17nPcuHEYN26cMeEQETmM6kOl/v73v2u27969G2PGjNH5noEDB2L79u1a23bt2oXo6Gi4ublZNF4iIlswKkElIiLjGTpUavr06Xj77beRkpKCadOmISsrC2vWrNFayKSsrAy5ubmaxxcuXEBOTg6aNWuGDh06WOW49u4F8vOB/v2BGqtZExEZhAkqEZGVGTpUKiIiAhkZGZg9ezaWL1+ONm3aYNmyZbjvvvs0bS5evIjevXtrnr/xxht44403MGTIEGRmZlrluNavB9atAxYtYoJKRKYxeKlTe+XsSxASke05+3nG1OOLjwd27ADWrgUmTbJAgETk8Cyy1CkREVFdWKSfiMyFCSoREZnFpUvyngkqEZmKCSoREZlMpQIuX5aPuYoUEZmKCSoREZnsyhWZpCoUQKtWto6GiBwdE1QiIjKZevypvz/A0qxEZCqWmaJGQ6VSoby83NZhkB1zc3ODq6urrcNwSBERwO7dQANrshAR6YUJKjk9IQQKCwtx7do1W4dCDsDPzw+BgYEmrXHfGDVvDgwbZusoiMhZMEElp6dOTlu3bg1vb28mHqSTEAI3b97E5f/N9AkKCrJxREREjRcTVHJqKpVKk5y2bNnS1uGQnfPy8gIAXL58Ga1bt+blfgPs2QP89hswcCDQpYutoyEiR8dJUuTU1GNOvb29bRwJOQr1zwrHKxtm7Vpg6lTgyy9tHQkROQMmqNQo8LI+6Ys/K8bhKlJEZE5MUImIyGRMUInInJigEhGRyZigEpE5MUElakSGDh2K5ORkvdufPXsWCoUCOTk5FovJnCZOnIixY8dqnht6vGSc0lLg6lX5mAkqEZkDE1QifalUQGYmsGGDvFepLPZRCoWi3tvEiRON2m96ejpeeuklvduHhIRAqVSiW7duRn2eIdLS0tC/f3/4+vqiefPm6Nq1K5588kmT9lnzeMPDw5GammpipFTT/ypzwc0NuO0228ZCRM6BZaaI9JGeDsyaBZw/X7UtOBhYuhRISDD7xymVSs3jTZs24YUXXsDJkyc129TlkNTKy8vhpsf6ki1atDAoDldXVwRaoUtsz549eOCBB7Bo0SKMHj0aCoUCubm5+Oqrr0zar6HHS8ZRX94PCABc2O1BRGbAUwlRQ9LTgXHjtJNTALhwQW5PTzf7RwYGBmpuvr6+UCgUmue3bt2Cn58fPv30UwwdOhSenp746KOPcOXKFTz44IMIDg6Gt7c3unfvjg0bNmjtt+Yl7/DwcCxatAiTJ09G8+bNERoainfffVfzes1L/JmZmVAoFPjqq68QHR0Nb29vxMTEaCXPAPDyyy+jdevWaN68OaZOnYo5c+agV69edR7v559/jjvuuANPP/00OnXqhI4dO2Ls2LF46623NG3mz5+PXr164Z133kFISAi8vb1x//3317tCWPXjHTp0KH777TfMnj1b0xNN5tGpk6yD+s47to6EiJwFE1Si+qhUsudUiNqvqbclJ1v0cn9dnnnmGcycORN5eXkYMWIEbt26haioKHz++ef4+eef8c9//hNJSUn4/vvv693Pm2++iejoaGRnZ2PGjBl47LHHcOLEiXrfM2/ePLz55ps4dOgQmjRpgsmTJ2te+/jjj/HKK6/gtddew+HDhxEaGoqVK1fWu7/AwEAcP34cP//8c73tTp8+jU8//RTbt2/Hjh07kJOTg8cff7ze96ilp6cjODgYCxcuhFKp1OqlJtP4+AB/+xtw9922joSInAUTVKL6HDhQu+e0OiGAggLZzsqSk5ORkJCAiIgItGnTBm3btsVTTz2FXr16oV27dnjiiScwYsQI/Pe//613P3fffTdmzJiBDh064JlnnoG/vz8yMzPrfc8rr7yCIUOGoEuXLpgzZw4OHjyIW7duAQDeeustTJkyBZMmTULHjh3xwgsvoHv37vXu74knnkDfvn3RvXt3hIeH44EHHsDatWtRWlqq1e7WrVv44IMP0KtXLwwePBhvvfUWNm7ciEL1NeZ6tGjRAq6urmjevLmmN5qIiOwTE1Si+ujby2aD3rjo6Git5yqVCq+88gp69OiBli1bolmzZti1axfOnTtX73569OiheaweSqBej16f96jXrFe/5+TJk+jXr59W+5rPa2ratCm++OILnD59Gs899xyaNWuGJ598Ev369cPNmzc17UJDQxEcHKx5PnDgQFRWVtYaYkDWtWMHsHo10EDHOxGR3pigEtXnf8mX2dqZUdOmTbWev/nmm/jPf/6Df/3rX/j666+Rk5ODESNGoKysrN791JxcpVAoUFlZqfd71GM5q7+n5vhOoWuIhA7t27fH1KlTsXr1avz000/Izc3Fpk2b6myv/hyOJ7Wt1auBadMAE+e0ERFpMEElqk9srJytX1cCpFAAISGynY0dOHAAY8aMwcMPP4yePXuiXbt2OHXqlNXj6NSpE3744QetbYcOHTJ4P+Hh4fD29saff/6p2Xbu3DlcvHhR8zwrKwsuLi7o2LGjXvt0d3eHygbjhZ1d9Vn8RETmwASVqD6urrKUFFA7SVU/T02V7WysQ4cO2L17Nw4ePIi8vDw8+uijeo3NNLcnnngCa9aswQcffIBTp07h5ZdfxtGjR+vt5Zw/fz7+9a9/ITMzE/n5+cjOzsbkyZNRXl6O4cOHa9p5enpiwoQJOHLkCA4cOICZM2di/Pjxeo8nDQ8Px/79+3HhwgUUFRWZfKwkcRUpIjI3JqhEDUlIADZvBtq21d4eHCy3W6AOqjGef/559OnTByNGjMDQoUMRGBiotaqStfzjH//A3Llz8dRTT6FPnz7Iz8/HxIkT4enpWed7hgwZgjNnzuCRRx5B586dER8fj8LCQuzatQudOnXStOvQoQMSEhJw9913Iy4uDt26dcOKFSv0jm3hwoU4e/Ys2rdvj1atWpl0nFSFCSoRmZtC6Ds4zM6VlJTA19cXxcXF8PHxsXU4ZCdu3bqF/Px8RERE1Jsg6UWlkrP1lUo55jQ21i56Th3B8OHDERgYiA8//NDofcyfPx+fffaZxZddre9nxtnPM8Yc340bQPPm8vH160CzZhYMkIgcnr7nGa4kRaQvV1dg6FBbR2H3bt68iVWrVmHEiBFwdXXFhg0bsGfPHuzevdvWoZEFqHtPmzZlckpE5sMElYjMSqFQICMjAy+//DJKS0vRqVMnpKWlYdiwYbYOjSyAl/eJyBKYoBKRWXl5eWHPnj1m3+/8+fMxf/58s++XTNO9u1zmlMURiMicmKASEZHRfH3lMqdERObUeBNUTnghIiIiskuNM0FNTwdmzdJeYz04WNa7tJOSQUREjuDzz+Xf+UOGAHqul0BE1KDGVwc1PR0YN047OQWACxfk9vR028RFROSA3nkH+Oc/gf37bR0JETmTxpWgqlSy51RX6Vf1tuRkjvYnItITZ/ETkSU0rgT1wAFNz6kKLsjEEGzAA8jEEKjgIpPUggLZjoiI6qVSAWfPyscXLvBveyIyn8aVoCqVAIB0/B3hOIs7kYmHsAF3IhPhOIt0/F2rHRGZZt26dfDz89M8nz9/Pnr16mWzeMh80tOB8HCgqEg+nz5dPucoKSIyh8aVoAYFIR1/xzhsxnlor6t+AW0xDptlkhoUZKMAyZ6pVEBmJrBhg7y3ZG+RQqGo9zZx4kSj9x0eHo7U1NQG22VnZ2PUqFFo3bo1PD09ER4ejsTERBSpMxIjPPXUU/jqq680zydOnIixY8cavT+yDQ7lJyJLa1Sz+FUxsZjl2hFCBdTMzQVcoEAlkl3fwpiYQLDgFFVn7cIPymq9+Js2bcILL7yAkydParZ5eXmZ/0OruXz5MoYNG4Z7770XO3fuhJ+fH/Lz87Ft2zbcvHnT6P02a9YMzbgepkNraCi/QiGH8o8Zw8p9RGS8RtWDeuCgK86r2qCuwxZwQYGqLQ4c5FmVqtiitygwMFBz8/X1hUKh0Nq2f/9+REVFwdPTE+3atcOCBQtQUVGhef/8+fMRGhoKDw8PtGnTBjNnzgQADB06FL/99htmz56t6Y3V5eDBgygpKcHq1avRu3dvRERE4K677kJqaipCQ0MBAJmZmVAoFPjiiy/Qs2dPeHp6on///jh27Fidx1X9Ev/8+fPxwQcfYOvWrZpYMjMzzfMFksVUG8qvE4fyE5E5NKoEVd+hpRyCSmr2WPhh586dePjhhzFz5kzk5ubinXfewbp16/DKK68AADZv3oz//Oc/eOedd3Dq1Cl89tln6N69OwAgPT0dwcHBWLhwIZRKpVZPbXWBgYGoqKjAli1bIHQdfDVPP/003njjDfz4449o3bo1Ro8ejfLy8gaP46mnnsL48eMxcuRITSwxMTEGfhtkbTyPEpE1NKoEVd+hpRyCSmr22Fv0yiuvYM6cOZgwYQLatWuH4cOH46WXXsI777wDADh37hwCAwMxbNgwhIaGol+/fpg2bRoAoEWLFnB1dUXz5s01vbG6DBgwAM8++yweeugh+Pv7Iz4+Hq+//jouXbpUq+2LL76I4cOHo3v37vjggw9w6dIlbNmypcHjaNasGby8vODh4aGJxd3d3YRvhqyB51EisoZGlaDGxspxg3Vc1YRCAYSEyHZEgH32Fh0+fBgLFy7UjOds1qwZpk2bBqVSiZs3b+L+++/HX3/9hXbt2mHatGnYsmWL1uV/fb3yyisoLCzEqlWr0KVLF6xatQqdO3eudQl/4MCBmsctWrRAp06dkJeXZ/Jxkn3ieZSIrKFRJaiurnJSC1D75Kp+nprKgf1UxR57iyorK7FgwQLk5ORobseOHcOpU6fg6emJkJAQnDx5EsuXL4eXlxdmzJiBwYMH63XZvaaWLVvi/vvvx5tvvom8vDy0adMGb7zxRoPvq2tsKzk+nkeJyBoaVYIKyBnXmzcDbbWrTCE4WG63xIxsclz22FvUp08fnDx5Eh06dKh1c3GR/6W9vLwwevRoLFu2DJmZmcjKytL0fLq7u0NlxKBZd3d3tG/fHn/++afW9u+++07z+OrVq/jll1/QuXNnvfdpTCxkWzyPEpGlNaoyU2oJCbIEyoED8tJsUJBMMPgXP9Wk7i0aN04mo9XnC9mqt+iFF17AqFGjEBISgvvvvx8uLi44evQojh07hpdffhnr1q2DSqVC//794e3tjQ8//BBeXl4ICwsDIOug7t+/Hw888AA8PDzg7+9f6zM+//xzbNy4EQ888AA6duwIIQS2b9+OjIwMvP/++1ptFy5ciJYtWyIgIADz5s2Dv7+/3rVNw8PDsXPnTpw8eRItW7aEr68v3NzcTP6OyPJ4HiUiS2qUCSogT6JDh9o6CnIE6t4iXXVQU1Ot31s0YsQIfP7551i4cCH+/e9/w83NDZ07d8bUqVMBAH5+fnj11VeRkpIClUqF7t27Y/v27WjZsiUAmVA++uijaN++PUpLS3XO0u/SpQu8vb3x5JNPoqCgAB4eHrj99tuxevVqJCUlabV99dVXMWvWLJw6dQo9e/bEtm3b9J7sNG3aNGRmZiI6Oho3btzA3r17MZT/MR0Gz6NEZCkK0VANGQdRUlICX19fFBcXw8fHx9bhkJ24desW8vPzERERAU9PT5P2pVKxt6i6zMxM3Hnnnbh69arWcqaOrr6fGWc/zzj78RGR7el7nmm0PahEhmJvERERkXU0uklSRERERGTfjEpQV6xYobn8FRUVhQMNVCkvLS3FvHnzEBYWBg8PD7Rv3x5r167VvL5u3TrNUofVb7du3TImPCKygqFDh0II4VSX94mIyD4YfIl/06ZNSE5OxooVKzBo0CC88847iI+PR25urmaN7prGjx+PS5cuYc2aNejQoQMuX75cq3C4j48PTp48qbXN1DGDREREROR4DE5QlyxZgilTpmhmDKempmLnzp1YuXIlFi9eXKv9jh07sG/fPpw5cwYtWrQAIEvL1KRQKOpcdpHIVJWVlbYOgRwEf1aIiGzPoAS1rKwMhw8fxpw5c7S2x8XF4eDBgzrfs23bNkRHR+Pf//43PvzwQzRt2hSjR4/GSy+9BC8vL027GzduICwsDCqVCr169cJLL72E3r171xlLaWkpSktLNc9LSkoMORRqJNzd3eHi4oKLFy+iVatWcHd35ypHpJMQAmVlZfj999/h4uKid6ksIiIyP4MS1KKiIqhUKgQEBGhtDwgIQGFhoc73nDlzBt988w08PT2xZcsWFBUVYcaMGfjjjz8041A7d+6MdevWoXv37igpKcHSpUsxaNAgHDlyBLfffrvO/S5evBgLFiwwJHxqhFxcXBAREQGlUomLFy/aOhxyAN7e3ggNDdWsykVERNZnVJmpmj1QQog6e6UqKyuhUCjw8ccfw9fXF4AcJjBu3DjNWuEDBgzAgAEDNO8ZNGgQ+vTpg7feegvLli3Tud+5c+ciJSVF87ykpAQhISHGHA45OXd3d4SGhqKiooLLalK9XF1d0aRJE/ayExHZmEEJqr+/P1xdXWv1ll6+fLlWr6paUFAQ2rZtq0lOASAyMhJCCJw/f15nD6mLiwv69u2LU6dO1RmLh4cHPDw8DAmfGjGFQgE3Nzcuo0lEROQADLqG5e7ujqioKOzevVtr++7duxETE6PzPYMGDcLFixdx48YNzbZffvkFLi4uCA4O1vkeIQRycnIQFBRkSHhERA7D0HJ9+/btQ1RUFDw9PdGuXTusWrWqVpu0tDR06dIFHh4e6NKlC7Zs2WKp8ImILMrgQVYpKSlYvXo11q5di7y8PMyePRvnzp3D9OnTAchL74888oim/UMPPYSWLVti0qRJyM3Nxf79+/H0009j8uTJmklSCxYswM6dO3HmzBnk5ORgypQpyMnJ0eyTiMiZqMv1zZs3D9nZ2YiNjUV8fDzOnTuns31+fj7uvvtuxMbGIjs7G88++yxmzpyJtLQ0TZusrCwkJiYiKSkJR44cQVJSEsaPH4/vv//eWodFRGQ+wgjLly8XYWFhwt3dXfTp00fs27dP89qECRPEkCFDtNrn5eWJYcOGCS8vLxEcHCxSUlLEzZs3Na8nJyeL0NBQ4e7uLlq1aiXi4uLEwYMHDYqpuLhYABDFxcXGHBIRUYPMdZ7p16+fmD59uta2zp07izlz5uhs/69//Ut07txZa9ujjz4qBgwYoHk+fvx4MXLkSK02I0aMEA888IDecfE8SkSWpu95RiGEELZNkc2juLgYfn5+KCgogI+Pj63DISInpJ6Mee3aNa1x9YYoKyuDt7c3/vvf/+Lvf/+7ZvusWbOQk5ODffv21XrP4MGD0bt3byxdulSzbcuWLRg/fjxu3rwJNzc3hIaGYvbs2Zg9e7amzX/+8x+kpqbit99+0xlLzXJ9xcXFCA0N5XmUiCxG3/OoUbP47dH169cBgDP5icjirl+/bnSCaky5vsLCQp3tKyoqUFRUhKCgoDrb1LVPoO5yfTyPEpGlNXQedZoEtU2bNigoKEDz5s0bdYkY9V8m7AGR+H1U4XehzZjvQwiB69evo02bNiZ/viHl+upqX3O7ofusWa6vsrISf/zxB1q2bNmoz6MA/79Ux++iCr8LbZY8jzpNglpfVYDGyMfHh/95quH3UYXfhTZDvw9je07VjCnXFxgYqLN9kyZN0LJly3rb1LVPQHe5Pj8/P30PpVHg/5cq/C6q8LvQZonzKJdKISKyImPK9Q0cOLBW+127diE6OlpT27euNnXtk4jInjlNDyoRkaNISUlBUlISoqOjMXDgQLz77ru1yvVduHAB69evBwBMnz4db7/9NlJSUjBt2jRkZWVhzZo12LBhg2afs2bNwuDBg/Haa69hzJgx2Lp1K/bs2YNvvvnGJsdIRGQKJqhOxsPDAy+++CJX2foffh9V+F1os+X3kZiYiCtXrmDhwoVQKpXo1q0bMjIyEBYWBgBQKpVaNVEjIiKQkZGB2bNnY/ny5WjTpg2WLVuG++67T9MmJiYGGzduxHPPPYfnn38e7du3x6ZNm9C/f3+rH58z4P+XKvwuqvC70GbJ78NpykwRERERkXPgGFQiIiIisitMUImIiIjIrjBBJSIiIiK7wgSViIiIiOwKE1QiIiIisitMUJ3E/PnzoVAotG6BgYG2Dstq9u/fj3vvvRdt2rSBQqHAZ599pvW6EALz589HmzZt4OXlhaFDh+L48eO2CdbCGvouJk6cWOtnZcCAAbYJ1sIWL16Mvn37onnz5mjdujXGjh2LkydParVpTD8bVD+eR3keVeN5tIqtzqNMUJ1I165doVQqNbdjx47ZOiSr+fPPP9GzZ0+8/fbbOl//97//jSVLluDtt9/Gjz/+iMDAQAwfPhzXr1+3cqSW19B3AQAjR47U+lnJyMiwYoTWs2/fPjz++OP47rvvsHv3blRUVCAuLg5//vmnpk1j+tmghvE8yvMowPNodTY7jwpyCi+++KLo2bOnrcOwCwDEli1bNM8rKytFYGCgePXVVzXbbt26JXx9fcWqVatsEKH11PwuhBBiwoQJYsyYMTaJx9YuX74sAIh9+/YJIRr3zwbVxvNoFZ5Hq/A8qs1a51H2oDqRU6dOoU2bNoiIiMADDzyAM2fO2Doku5Cfn4/CwkLExcVptnl4eGDIkCE4ePCgDSOznczMTLRu3RodO3bEtGnTcPnyZVuHZBXFxcUAgBYtWgDgzwbVxvOobvy/UhvPo5Y9jzJBdRL9+/fH+vXrsXPnTrz33nsoLCxETEwMrly5YuvQbK6wsBAAEBAQoLU9ICBA81pjEh8fj48//hhff/013nzzTfz444+46667UFpaauvQLEoIgZSUFNxxxx3o1q0bAP5skDaeR+vG/yvaeB61/Hm0ifFhkj2Jj4/XPO7evTsGDhyI9u3b44MPPkBKSooNI7MfCoVC67kQota2xiAxMVHzuFu3boiOjkZYWBi++OILJCQk2DAyy/q///s/HD16FN98802t1/izQQDPo/rg/xWJ51HLn0fZg+qkmjZtiu7du+PUqVO2DsXm1LNwa/4ld/ny5Vp/8TVGQUFBCAsLc+qflSeeeALbtm3D3r17ERwcrNnOnw2qD8+jVfh/pX48j5r/Z4MJqpMqLS1FXl4egoKCbB2KzUVERCAwMBC7d+/WbCsrK8O+ffsQExNjw8jsw5UrV1BQUOCUPytCCPzf//0f0tPT8fXXXyMiIkLrdf5sUH14Hq3C/yv143nU/D8bvMTvJJ566ince++9CA0NxeXLl/Hyyy+jpKQEEyZMsHVoVnHjxg2cPn1a8zw/Px85OTlo0aIFQkNDkZycjEWLFuH222/H7bffjkWLFsHb2xsPPfSQDaO2jPq+ixYtWmD+/Pm47777EBQUhLNnz+LZZ5+Fv78//v73v9swast4/PHH8cknn2Dr1q1o3ry55i98X19feHl5QaFQNKqfDaofz6M8j6rxPFrFZudRo+f/k11JTEwUQUFBws3NTbRp00YkJCSI48eP2zosq9m7d68AUOs2YcIEIYQsg/Hiiy+KwMBA4eHhIQYPHiyOHTtm26AtpL7v4ubNmyIuLk60atVKuLm5idDQUDFhwgRx7tw5W4dtEbq+BwDi/fff17RpTD8bVD+eR3keVeN5tIqtzqOK/304EREREZFd4BhUIiIiIrIrTFCJiIiIyK4wQSUiIiIiu8IElYiIiIjsChNUIiIiIrIrTFCJiIiIyK4wQSUiIiIiu8IElYiIiIjsChNUIiIiIrIrTFCJiIiIyK4wQSUiIiIiu/L/j9Lkl4bU2PoAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "accuracies = []\n", + "rf_ = []\n", + "k = range(1,21)\n", + "for i in k:\n", + " rf = RandomForestClassifier(max_depth=i, n_estimators=15, max_features=100)\n", + " rf.fit(X_train, y_train)\n", + " y_pred_train = rf.predict(X_train)\n", + " y_pred_test = rf.predict(X_test)\n", + " accuracy_train = accuracy_score(y_train, y_pred_train)\n", + " accuracy_test = accuracy_score(y_test, y_pred_test)\n", + " \n", + " rf_.append(rf)\n", + " accuracies.append([accuracy_train, accuracy_test])\n", + " \n", + "fig = plt.figure(figsize=(8,6))\n", + "ax = fig.add_subplot(1,2,1)\n", + "ax.scatter(k, [elmnt[0] for elmnt in accuracies], c='r', label='Training Split')\n", + "ax.scatter(k, [elmnt[1] for elmnt in accuracies], c='b', label='Test Split')\n", + "ax.legend()\n", + "ax1 = fig.add_subplot(1,2,2)\n", + "ax1.plot(k, [abs(elmnt[0]-elmnt[1]) for elmnt in accuracies], 'bo--', label='Absolute Difference')\n", + "ax1.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "0dec508a-11be-493c-8c51-170703cd8c86", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accuracy Test: 0.9527777777777777\n", + "Accuracy Train: 0.9976190476190476\n" + ] + } + ], + "source": [ + "rf = rf_[8]\n", + "y_pred_test = rf.predict(X_test)\n", + "accuracy_test = accuracy_score(y_test, y_pred_test)\n", + "print(\"Accuracy Test:\", accuracy_test)\n", + "\n", + "y_pred_train = rf.predict(X_train)\n", + "accuracy_train = accuracy_score(y_train, y_pred_train)\n", + "print('Accuracy Train:', accuracy_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "1ef007a8-01c1-4b57-8c25-4cf7bf0e9003", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "##### Create the confusion matrix\n", + "cm = confusion_matrix(y_test, y_pred_test)\n", + "\n", + "ConfusionMatrixDisplay(confusion_matrix=cm).plot();" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "e07fcafc-99c6-4687-9279-154db8f98069", + "metadata": {}, + "outputs": [], + "source": [ + "pickle.dump(rf, open('Models/Classifier_Model_Binary_RF'+'.pickle', 'wb'))" + ] + }, + { + "cell_type": "markdown", + "id": "2cc06173-54a1-40d1-954b-34c67dc414ff", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "## Local Extracted Features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51aea9b5-fed6-4155-bd61-e1c95ed564f7", + "metadata": {}, + "outputs": [], + "source": [ + "# mixing data rows --> data shouldnt be sorted for splitting \n", + "data_sample = local_features.copy().sample(frac=1).reset_index(drop=True)\n", + "X = data_sample.drop(['label', 'img', 'pore_number', 'image_index'], axis=1)\n", + "Y = data_sample['label']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d15e796-5101-4d24-a8f1-18b214722997", + "metadata": {}, + "outputs": [], + "source": [ + "# scaling / normalizing the features\n", + "\n", + "# copy the dataframe\n", + "df_norm = X.copy()\n", + "# apply min-max scaling\n", + "for column in df_norm.columns:\n", + " df_norm[column] = (df_norm[column] - df_norm[column].min()) / (df_norm[column].max() - df_norm[column].min())\n", + "\n", + "X = df_norm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d5a8c04-a778-411a-8e8b-45171533ba7a", + "metadata": {}, + "outputs": [], + "source": [ + "sampling = X.copy()\n", + "sampling['label'] = Y\n", + "sampling.sample(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a042f2c-25b3-487f-9528-ad5b35cf1980", + "metadata": {}, + "outputs": [], + "source": [ + "# splitting data into training and test partition\n", + "X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bce3a20d-e3de-4bf1-8e33-d8fea848218c", + "metadata": {}, + "outputs": [], + "source": [ + "accuracies = []\n", + "rf_ = []\n", + "k = range(1,21)\n", + "for i in k:\n", + " rf = RandomForestClassifier(max_depth=i, n_estimators=200, max_features=5)\n", + " rf.fit(X_train, y_train)\n", + " y_pred_train = rf.predict(X_train)\n", + " y_pred_test = rf.predict(X_test)\n", + " accuracy_train = accuracy_score(y_train, y_pred_train)\n", + " accuracy_test = accuracy_score(y_test, y_pred_test)\n", + " \n", + " rf_.append(rf)\n", + " accuracies.append([accuracy_train, accuracy_test])\n", + " \n", + "fig = plt.figure(figsize=(8,6))\n", + "ax = fig.add_subplot(1,2,1)\n", + "ax.scatter(k, [elmnt[0] for elmnt in accuracies], c='r', label='Training Split')\n", + "ax.scatter(k, [elmnt[1] for elmnt in accuracies], c='b', label='Test Split')\n", + "ax.legend()\n", + "ax1 = fig.add_subplot(1,2,2)\n", + "ax1.plot(k, [abs(elmnt[0]-elmnt[1]) for elmnt in accuracies], 'bo--', label='Absolute Difference')\n", + "ax1.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77e4accf-be21-4bed-8362-64698a1827d6", + "metadata": {}, + "outputs": [], + "source": [ + "rf = rf_[5]\n", + "y_pred_test = rf.predict(X_test)\n", + "accuracy_test = accuracy_score(y_test, y_pred_test)\n", + "print(\"Accuracy Test:\", accuracy_test)\n", + "\n", + "y_pred_train = rf.predict(X_train)\n", + "accuracy_train = accuracy_score(y_train, y_pred_train)\n", + "print('Accuracy Train:', accuracy_train)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a07e6323-6ea7-4d75-9836-831eaf4eb132", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the confusion matrix\n", + "cm = confusion_matrix(y_test, y_pred_test)\n", + "\n", + "ConfusionMatrixDisplay(confusion_matrix=cm).plot();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e26651e-700a-4ca2-b5fa-1ba764490ba2", + "metadata": {}, + "outputs": [], + "source": [ + "# plt.figure(figsize=(50,50))\n", + "corr = sampling.corr(numeric_only=True)\n", + "corrplot(corr, size_scale=500, marker=\"s\")\n", + "# plt.savefig('Images/Results/{}/KorrPlot.pdf'.format(dataset))\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6619c79f-ea78-4da2-9f34-2ca51430f5c0", + "metadata": { + "tags": [] + }, + "source": [ + "# Results" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3b3eb2d2-1e89-45b2-9eac-1808cc7e6529", + "metadata": {}, + "outputs": [], + "source": [ + "results = data_red.dataframe" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6dab6db2-a569-448d-a0b6-bc9e7f63074a", + "metadata": {}, + "outputs": [], + "source": [ + "results['labels'] = labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1fb088d5-df6e-474c-a87d-661446599ef2", + "metadata": {}, + "outputs": [], + "source": [ + "results['labels'] = labels\n", + "results['predicted_kmeans'] = labels_pred_kmeans" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8a86641-f5b7-463a-9ba8-568cbb9838e2", + "metadata": {}, + "outputs": [], + "source": [ + "results['predicted_dbscan'] = labels_pred_dbscan" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fb212a8-fe1b-4145-bc50-e2050ba5604c", + "metadata": {}, + "outputs": [], + "source": [ + "results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc45ab75-9fd8-4d2c-8bf3-1f8f5d7a9101", + "metadata": {}, + "outputs": [], + "source": [ + "sampling.to_csv('Images/Ti6Al4V_Analysis/Ti6Al4V_labeled_prepared_data.csv', sep=';', decimal=\",\", index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "71a45f04-947a-4f4c-8790-744ac7b48470", + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, + "source": [ + "# ANN Model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c33d6041-736a-442d-906b-c3960e20276e", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.neural_network import MLPClassifier" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc56b488-502e-4116-9b16-e0b8294f18ef", + "metadata": {}, + "outputs": [], + "source": [ + "clf = MLPClassifier(solver='lbfgs', alpha=1e-5, hidden_layer_sizes=(60,40,20), random_state=1, max_iter=10000000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b0ff09b-7d3b-4984-aa44-affcae58c91a", + "metadata": {}, + "outputs": [], + "source": [ + "clf.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2adb423a-91c5-4492-bdef-51dfd889a218", + "metadata": {}, + "outputs": [], + "source": [ + "mlp_predicted = clf.predict(X_train)\n", + "# Create the confusion matrix\n", + "cm = confusion_matrix(y_train, mlp_predicted)\n", + "\n", + "ConfusionMatrixDisplay(confusion_matrix=cm).plot();" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6177872-9b3b-424c-bf93-4ae2b995e996", + "metadata": {}, + "outputs": [], + "source": [ + "mlp_predicted = clf.predict(X_test)\n", + "# Create the confusion matrix\n", + "cm = confusion_matrix(y_test, mlp_predicted)\n", + "\n", + "ConfusionMatrixDisplay(confusion_matrix=cm).plot();" + ] + }, + { + "cell_type": "markdown", + "id": "25d0b1a4-4617-43a4-acd7-92367f983b3a", + "metadata": { + "tags": [] + }, + "source": [ + "# Final Model Test" + ] + }, + { + "cell_type": "code", + "execution_count": 181, + "id": "6572ffdd-569e-4fad-b394-295a926d3912", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "436 pores found!\n" + ] + } + ], + "source": [ + "# load pore images from different material to classify\n", + "images = [filepath for filepath in glob.iglob('Images/Ti6Al4V-Stichprobe_4/pores/405/*.jpg')]\n", + "\n", + "# read and resize pore binary image\n", + "img = [(cv.resize(cv.imread(image, cv.IMREAD_GRAYSCALE), dsize=(100, 100), interpolation=cv.INTER_CUBIC)) for image in images]\n", + "print('{} pores found!'.format(len(images)))" + ] + }, + { + "cell_type": "code", + "execution_count": 182, + "id": "c00e4cd0-9f6b-443e-b760-dc09558a3a96", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Varianz explained by PCA model with 249 Components is 96.833 %.\n" + ] + } + ], + "source": [ + "# push image data in pipeline\n", + "\n", + "# flattening images into one long vector\n", + "data_px = Preprocessing(img)\n", + "\n", + "# reducing dimensions with pretrained pca model\n", + "data_red = DimensionReductionPCA(data_px.dataframe, k=249, pca_model='Models/PCA_Model_Binary_RF')\n", + "data_red.pca_explain()" + ] + }, + { + "cell_type": "code", + "execution_count": 183, + "id": "d7a0a0fd-d9ad-4cf0-93d0-fedd20a99b0b", + "metadata": {}, + "outputs": [], + "source": [ + "# load classifier\n", + "classifier = pickle.load(open('Models/Classifier_Model_Binary_RF.pickle', 'rb'))" + ] + }, + { + "cell_type": "code", + "execution_count": 184, + "id": "1236efa7-4c9c-48c9-86ec-f3068a25c576", + "metadata": {}, + "outputs": [], + "source": [ + "# classify pores\n", + "y_pred = classifier.predict(data_red.dataframe)" + ] + }, + { + "cell_type": "code", + "execution_count": 185, + "id": "70392f14-d54d-41b3-991f-f323f3db994c", + "metadata": {}, + "outputs": [], + "source": [ + "# save pores depending by predicted label\n", + "try: \n", + " os.makedirs('Images/Ti6Al4V-Stichprobe_4/results/405/0')\n", + " os.makedirs('Images/Ti6Al4V-Stichprobe_4/results/405/1')\n", + " os.makedirs('Images/Ti6Al4V-Stichprobe_4/results/405/2')\n", + " \n", + "except OSError:\n", + " pass\n", + " \n", + "for i, image in enumerate(images):\n", + " label = y_pred[i]\n", + " img = cv.resize(cv.imread(image), dsize=(100, 100), interpolation=cv.INTER_CUBIC)\n", + " cv.imwrite('Images/Ti6Al4V-Stichprobe_4/results/405/{}/{}.jpg'.format(label, i), img)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Models/Classifier_Model_Binary_RF.pickle b/Models/Classifier_Model_Binary_RF.pickle new file mode 100644 index 0000000..3b433a0 Binary files /dev/null and b/Models/Classifier_Model_Binary_RF.pickle differ diff --git a/Models/PCA_Model_Binary_RF.pickle b/Models/PCA_Model_Binary_RF.pickle new file mode 100644 index 0000000..ab775da Binary files /dev/null and b/Models/PCA_Model_Binary_RF.pickle differ diff --git a/analyzer/__init__.py b/analyzer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/analyzer/__pycache__/__init__.cpython-310.pyc b/analyzer/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..9af5224 Binary files /dev/null and b/analyzer/__pycache__/__init__.cpython-310.pyc differ diff --git a/analyzer/__pycache__/__init__.cpython-38.pyc b/analyzer/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..4a3b72b Binary files /dev/null and b/analyzer/__pycache__/__init__.cpython-38.pyc differ diff --git a/analyzer/__pycache__/__init__.cpython-39.pyc b/analyzer/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..124dba7 Binary files /dev/null and b/analyzer/__pycache__/__init__.cpython-39.pyc differ diff --git a/analyzer/__pycache__/classificator.cpython-39.pyc b/analyzer/__pycache__/classificator.cpython-39.pyc new file mode 100644 index 0000000..ef20f4d Binary files /dev/null and b/analyzer/__pycache__/classificator.cpython-39.pyc differ diff --git a/analyzer/__pycache__/featureextraction.cpython-310.pyc b/analyzer/__pycache__/featureextraction.cpython-310.pyc new file mode 100644 index 0000000..d5e7723 Binary files /dev/null and b/analyzer/__pycache__/featureextraction.cpython-310.pyc differ diff --git a/analyzer/__pycache__/featureextraction.cpython-39.pyc b/analyzer/__pycache__/featureextraction.cpython-39.pyc new file mode 100644 index 0000000..95b3ce1 Binary files /dev/null and b/analyzer/__pycache__/featureextraction.cpython-39.pyc differ diff --git a/analyzer/__pycache__/features.cpython-310.pyc b/analyzer/__pycache__/features.cpython-310.pyc new file mode 100644 index 0000000..e5582fb Binary files /dev/null and b/analyzer/__pycache__/features.cpython-310.pyc differ diff --git a/analyzer/__pycache__/features.cpython-38.pyc b/analyzer/__pycache__/features.cpython-38.pyc new file mode 100644 index 0000000..64cc421 Binary files /dev/null and b/analyzer/__pycache__/features.cpython-38.pyc differ diff --git a/analyzer/__pycache__/features.cpython-39.pyc b/analyzer/__pycache__/features.cpython-39.pyc new file mode 100644 index 0000000..90f209e Binary files /dev/null and b/analyzer/__pycache__/features.cpython-39.pyc differ diff --git a/analyzer/classificator.py b/analyzer/classificator.py new file mode 100644 index 0000000..4e608a4 --- /dev/null +++ b/analyzer/classificator.py @@ -0,0 +1,191 @@ +import cv2 as cv +import numpy as np +from kneed import KneeLocator +from sklearn.cluster import KMeans +from sklearn.cluster import DBSCAN +from sklearn.neighbors import NearestNeighbors +from sklearn.decomposition import PCA +import matplotlib.pyplot as plt +import pickle +import pandas as pd + + + +class PoreSeperator(): + def __init__(self, contour, img, segmentsize=0.1): + self.image = img + self.contour = contour + self.segmentsize = segmentsize + self.pore = self.__seperate() + + def __seperate(self): + width = round(self.segmentsize * self.image.shape[1]) + height = round(self.segmentsize * self.image.shape[0]) + blanc = np.zeros_like(self.image) + filled = cv.fillPoly(blanc, [self.contour], color=(255,255,255)) + + M = cv.moments(self.contour) + x = int(M['m10'] / M['m00']) + y = int(M['m01'] / M['m00']) + + filled = filled[y-round(height/2):y+round(height/2), x-round(width/2):x+round(width/2)] + + return filled + + def check_size(self): + if self.pore.shape[0] == round(self.segmentsize * self.image.shape[0]) and self.pore.shape[1] == round(self.segmentsize * self.image.shape[1]): + return True + else: + return False + + def save(self, name): + cv.imwrite(name, self.pore) + + + +class Preprocessing(): + def __init__(self, pores): + self.pores = pores + self.pores_reshpd = [np.float32(pore.reshape((-1,1))).T for pore in pores] # Bilddaten zu Vektor umwandeln + self.data = [data[0] for data in self.pores_reshpd] + self.dataframe = self.__create_dataframe() + + def __create_dataframe(self): + data_dict = {} + for i in range(len(self.data[0])): + data_dict['x{}'.format(i)] = [data[i] for data in self.data] + return pd.DataFrame.from_dict(data_dict) + + + +class DimensionReductionPCA(): + def __init__(self, data, pca_model=None, k=3): + self.data = data + self.k = k + self.pca_model, self.explanation = self.__create_pca_model() if pca_model == None else self.__load_pca_model(pca_model) + self.pca = self.__calc_pca() + self.dataframe = self.__create_dataframe() + + def __create_pca_model(self): + if self.data.shape[0] < self.data.shape[1]: + n = self.data.shape[0] + else: + n = self.data.shape[1] + + pca_ = PCA(n_components=n, random_state=2020) + pca_.fit(self.data) + explanation = np.cumsum(pca_.explained_variance_ratio_ * 100) + + pca_m = PCA(n_components=self.k, random_state=2020) + pca_m.fit(self.data) + return pca_m, explanation + + def __calc_pca(self): + return self.pca_model.transform(self.data) + + def __load_pca_model(self, name): + pca_ = pickle.load(open('{}.pickle'.format(name), 'rb')) + explanation = np.cumsum(pca_.explained_variance_ratio_ * 100) + return pca_, explanation + + def __create_dataframe(self): + data_dict = {} + for i in range(len(self.pca[0])): + data_dict['pca{}'.format(i)] = [elmnt[i] for elmnt in self.pca] + return pd.DataFrame.from_dict(data_dict) + + def save_pca_model(self, name): + pickle.dump(self.pca_model, open('{}.pickle'.format(name), 'wb')) + print('PCA Model saved as {}.pickle'.format(name)) + + def pca_explain(self): + print('Varianz explained by PCA model with {} Components is {:.3f} %.'.format(self.k, self.explanation[self.k])) + + def scale(self): + # copy the dataframe + df_norm = self.dataframe.copy() + # apply min-max scaling + for column in df_norm.columns: + df_norm[column] = (df_norm[column] - df_norm[column].min()) / (df_norm[column].max() - df_norm[column].min()) + + return df_norm + + + +class DBSCANClassifier(): + def __init__(self, data, n_neighbors=2, min_samples = 3, knee=None): + self.data = data + self.n_neighbors = n_neighbors + self.min_samples = min_samples + self.knee = self.__calc_knee() if knee == None else knee + self.n_clusters = None + self.n_outliers = None + self.labels = self.__dbscan() + + def __calc_knee(self): + # finding best epsilon for DBSCAN: https://iopscience.iop.org/article/10.1088/1755-1315/31/1/012012/pdf + nbrs = NearestNeighbors(n_neighbors = self.n_neighbors, metric='euclidean').fit(self.data) # Anzahl an Nachbarn sollte etwa das Doppelte von der Featureanzahl sein + neigh_dist, neigh_ind = nbrs.kneighbors(self.data) + sort_neigh_dist = np.sort(neigh_dist, axis=0) + + k_dist = sort_neigh_dist[:, self.n_neighbors-1] + x = [i for i in range(len(k_dist))] + + kneedle = KneeLocator(x=x, y=k_dist, S=1.0, curve='concave', direction='increasing', online=True) + knee = kneedle.knee_y + return knee + + def __dbscan(self): + knee = ( self.knee if self.knee != None else 0.1 ) + db = DBSCAN(eps=knee, min_samples=self.min_samples).fit(self.data) + labels = db.labels_ + + i = 0 + for element in labels: # Anzahl an Ausreißern zählen + if element==-1: + i+=1 + + self.n_clusters = len(set(labels)) - (1 if -1 in labels else 0) + self.n_noise = list(labels).count(-1) + return labels + + + +class KMeansClassifier(): + def __init__(self, data): + self.data = data + + def find_knee(self, k=5): + K = range(1, k) + distortions = [] + + for k in K: + kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, max_iter=100, random_state=None) + kmeans.fit(self.data) + distortions.append(kmeans.inertia_) + + # kneedle = KneeLocator(x=K, y=distortions, S=1.0, curve='concave', direction='decreasing', online=True) + + plt.figure(figsize=(16,8)) + plt.plot(K, distortions, 'bx-') + plt.xlabel('k') + plt.ylabel('Distortion') + plt.title('Am Ellenbogen liegt die perfekte Anzahl an Clustern') + plt.show() + + # return distortions# kneedle.knee_y + + def train(self, k=3): + kmeans = KMeans(n_clusters=k, init='k-means++', n_init=10, max_iter=100, random_state=None) + kmeans.fit(self.data) + self.model = kmeans + + def predict(self, data): + self.model.predict(data) + clusters = self.model.labels_ + + return clusters + + def save_model(self, name): + pickle.dump(self.model, open(name+'.pickle', 'wb')) + print('Model saved as {}.pickle!'.format(name)) \ No newline at end of file diff --git a/analyzer/featureextraction.py b/analyzer/featureextraction.py new file mode 100644 index 0000000..4c4ae31 --- /dev/null +++ b/analyzer/featureextraction.py @@ -0,0 +1,828 @@ +######################################################## F E A T U R E E X T R A K T I O N ######################################################## +###### ###### +###### A U T H O R I N F O R M A T I O N S ###### +###### Mika Altmann ###### +###### 20th of February, 2023 ###### +###### Leibniz-Institute for Materials Science, Bremen, Germany ###### +###### ###### +###### D E S C R I B T I O N ###### +###### These functions containing methods to extract multiple features from metallurgical micrographs, especially for porosity evaluations ###### +###### in PBF-LB/M processes. ###### +###### ###### +##################################################################################################################################################### + +import cv2 as cv +import numpy as np +from numpy import median +import matplotlib.pyplot as plt +from pywt import dwt2 +import pywt +import pandas as pd +import math +from scipy.stats import skew +import imutils + +##################################################################################################################################################### + +###### Alle Konturen in dem Schliffbild bestimmen ###### +def get_Contours(image): + ### Bild in Graustufen umwandeln, wenn es ein RGB Bild ist ### + try: + image = cv.cvtColor(image, cv.COLOR_RGB2GRAY) + except: + image=image + + ### Weichzeichnen mit Gaussfilter und binarisieren ### + # bei 5x5 Gausszeichner gehen viele kleine Poren/Defekte verloren + # bei 3x3 Gausszeichner werden mehr kleine Poren/Defekte erkannt + img_blur = cv.GaussianBlur(image, (5, 5), 0) + threshold, img_binary = cv.threshold(img_blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU) + + ### Bild zuschneiden ### + # 1/3 des Bildes wird oben und 1/3 unten weg geschnitte + # es bleibt nur noch ein Streifen in der Mitte (idealerweise stabilder Prozesszustand) + # Entfernung der Skalen + height = img_binary.shape[0] + width = img_binary.shape[1] + + #upper_crop = round(height * 1/3) + #lower_crop = round(height * 2/3) + + #Center Crop + # gleich großen Untersuchungsbereich aus den Bildern ausschneiden + w = round( 2000 * 1.79173 ) + h = round( 2000 * 1.79173 ) + + x = round( width/2 - w/2 ) + y = round( height/2 - h/2 ) + + img_binary = img_binary[y:y+h, x:x+w] + img_binary = cv.copyMakeBorder(img_binary, 5, 5, 5, 5, cv.BORDER_CONSTANT, None, value=0) + + ###### Konturen und deren Hierarchien bestimmen und speichern ###### + # contours, hierarchy = cv.findContours(img_binary, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE) + contours, hierarchy = cv.findContours(img_binary, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + +# ### Winkel der Probenkontur bestimmen ### +# contour_area = [cv.contourArea(contour) for contour in contours] +# specimen_index = [i for i in range(len(contours)) if contour_area[i] == max(contour_area)][0] +# specimen_bounding = cv.minAreaRect(contours[specimen_index]) +# specimen_angle = specimen_bounding[2] + +# ### originales Bild rotieren ### +# img_rot = imutils.rotate(image, angle= -1*specimen_angle) + + ### hierarchy ### + # hierarchy[i][0]: the index of the next contour of the same level + # hierarchy[i][1]: the index of the previous contour of the same level + # hierarchy[i][2]: the index of the first child + # hierarchy[i][3]: the index of the parent + + # print('Anzahl an Konturen: {}'.format(len(contours))) + + return contours, hierarchy[0], img_binary, threshold + +##################################################################################################################################################### + +###### Bounding Boxen und Polygone um alle Poren/Defekte legen ###### +def get_BoundingBox(contours): + # leere Arrays erzeugen in denen alle Boundingboxen und Polygone gepseichert werden + bounding_poly = [None]*len(contours) + bounding_rect = [None]*len(contours) + + # für jede Kontur eine Boundingbox und ein umschließendes Polygon erzeugen + for i, contour in enumerate(contours): + bounding_poly[i] = cv.approxPolyDP(contour, 1, True) + bounding_rect[i] = cv.boundingRect(bounding_poly[i]) + + # Koordinaten der Bounding Boxen zurück geben + return bounding_rect + +##################################################################################################################################################### + +###### die Poren mithilfe von den Boundingboxen segmentieren ###### +def segment_Contours(bounding_rect, binary_image): + # leeren Array für alle Region of Interests erzeugen + roi = [None]*len(bounding_rect) + + # Über alle Boundingboxen iterieren und die ROIs in dem Array speichern + for i, rect in enumerate(bounding_rect): + roi[i] = binary_image[int(bounding_rect[i][1]): int(bounding_rect[i][1]+int(bounding_rect[i][3])), + int(bounding_rect[i][0]): int(bounding_rect[i][0])+int(bounding_rect[i][2])] + + # alle einzeln segmentierten Konturen zurückgeben + return roi + +##################################################################################################################################################### + +###### Nach Poren und Partikeln sortieren ###### +# Die Konuturen sind sowohl Partikel als auch Poren +# Partikel können dabei auf der Probenaußenseite als auch innerhalb von Poren vorliegen +# eine Unterscheidung zwischen Partikeln und Poren ist zwingend notwendig für die Ableitung statistischer Größen + +def sort_Contours(contours, hierarchy, binary_image): + + ### Non Parents finden ### + # haben keine Eltern-Kontur + # sind damit die Probenkontur oder Anhaftungen außerhalb der Probenkontur + # Partikel innerhalb von Poren haben allerdings ebenfalls keine Eltern-Kontur + # es müssen innere von äußeren Partikeln getrennt werden + + non_parents_shape = [] + non_parents_index = [] + + for i, contour in enumerate(contours): + if hierarchy[i][3] <= 0: + non_parents_shape.append(contour.shape[0]*contour.shape[1]) # Fläche aller non_parents berechnen (Größe der Boundingbox) + non_parents_index.append(i) # Liste der "realen" Indizes in non_parents_index speichern + + ### Probenkontur finden ### + # erheblich größer als alle anderen Konturen + # Index in der Liste alle Konut + specimen_contour_shape = max(non_parents_shape) + + for i, shape in enumerate(non_parents_shape): + if shape == specimen_contour_shape: + specimen_contour_index = non_parents_index[i] + non_parents_shape.pop(i) # Probenkontur aus den non_parents_shape löschen + non_parents_index.pop(i) # Probenkontur aus den non_parents_index löschen + + ### Kontur und Hierarchy der Probenkontur abspeichern ### + # specimen_contour[0] enthält die Kontur + # specimen_contour[1] enthält die hierarchy + # specimen_contour[2] enthält den realen Index + specimen_contour = [contours[specimen_contour_index], hierarchy[specimen_contour_index], specimen_contour_index] + +# ### inner Poren entfernen und Differenz bestimmen #### +# img = np.zeros_like(binary_image) +# particle_contours = [contours[index] for index in non_parents_index] +# img = cv.drawContours(img, particle_contours, -1, (255,255,255), -1) # alle Partikel (non_parents) in weiß einzeichnen +# img = cv.drawContours(img, specimen_contour[0], 0, (2555,255,255), -1) +# ## Konturen der äußeren Partikel finden ## +# outer_particles, _ = cv.findContours(img, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE) +# outer_particles_shape = [contour.shape[0]*contour.shape[1] for contour in outer_particles] +# ## Indizes der äußeren Partikel bestimmen ## +# outer_particles_index = [non_parents_index[i] for i, shape in enumerate(non_parents_shape) if shape in outer_particles_shape] +# inner_particles_index = [non_parents_index[i] for i, shape in enumerate(non_parents_shape) if not shape in outer_particles_shape] + +# print('Anzahl an Partikeln: {}'.format(len(non_parents_shape))) +# print('Anzahl äußerer Partikel: {}'.format(len(outer_particles))) +# print('Anzahl innerer Partikel:{}'.format(len(inner_particles_index))) +# cv.imwrite('Images/outer_Particles.jpg', img) + + ### Poren finden ### + # müssen alle innerhalb der Probenkontur liegen + # müssen als Eltern-Kontur die Probenkontur haben + inner_pores_index = [] + + for i, contour in enumerate(hierarchy): + if contour[3] == specimen_contour_index: # Filtern ob die Pore als Elternkontur die Probenkontur hat + inner_pores_index.append(i) + + ### Ausgeben von Informationen über die Ergebnisse ### + # print('Probenkontur-Index: {}'.format(specimen_contour_index)) + # print('Anzahl an Partikelanhaftungen: {}'.format(len(non_parents_index))) + # print('Anzahl an innerer Poren: {}'.format(len(inner_pores_index))) + + ### Probenkontur und Anhaftungen zurückgeben ### + # Rückgabe der realen Indizes der anhaftenden Partikel + # Größen etc. der Anhaftungen können dann über den Index aus der Konturliste ausgelesen werden. + outer_particles_index = non_parents_index # Anhaftungen sind alle Konturen ohne Eltern und ohne die Probenkontur + return specimen_contour, outer_particles_index, inner_pores_index + +##################################################################################################################################################### + +###### Porenfeatures bestimmen und Porenzuschnitte speichern ###### +# Schliffbild maskieren, sodass alle äußeren Partikel weg fallen +def get_Pore_Features(binary_img, specimen_contour, circularity_threshold, save_pores, save_path, sizeFilter): + ### idealisiertes Schliffbild erzeugen ### + # leeres Bild [None] --> Partikel in Schwarz [0] --> Poren in weiß [255] ==> äußere Partikel verschwinden + # Poren in äußeren Partikeln könnten im idealisierten Bild auftreten + # mask = specimen_contour # Probenkontur als Maske --> äußere Partikel fallen weg + # blanc = np.zeros_like(binary_img) # leeres schwarzes Bild erzeugen + # color = [255,255,255] # Farbe (Weiß) für zu maskierenden Bereich + # cv.fillPoly(blanc, [mask], color) # zu maskierenden Bereich einfärben --> alles innerhalb der Maske bzw. weiße bleibt bestehen + # result = cv.bitwise_and(binary_img, blanc) # Binärbild maskieren, nur Probe mit inneren Poren bleibt bestehen + + ### Konturen suchen ### + # kein bluring oder binarisieren notwendig, wurde bei binary_image bereits angewendet + # contours, hierarchy = cv.findContours(result, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + contours, hierarchy = cv.findContours(binary_img, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + + ### Poren und innere Partikel separieren ### + # Partikel haben nicht die Probe als Elternkontur sondern die Poren + # Probenkontur finden + contour_area = [cv.contourArea(contour) for contour in contours] + specimen_index = [i for i in range(len(contours)) if contour_area[i] == max(contour_area)][0] + pores_index = [i for i, hierarch in enumerate(hierarchy[0]) if hierarch[3] == specimen_index] + # Konturflächenfilter + if sizeFilter != None: + for index in pores_index: + if contour_area[index] <= 5: + pores_index.remove(index) + # (pores_index.remove(index) for index in pores_index if contour_area[index] < sizeFilter) + + ### Konturen der Poren als Liste speichern ### + pores = [contours[index] for index in pores_index] + + ## Anzahl und Größe der inneren Partikel für jede Pore bestimmen + # Partikel mit Poren finden + particle_pores = [index for index in pores_index if hierarchy[0, index, 2] >= 0] + # Partikel aus allen Konturen selektieren + particles = [index for index in range(len(contours)) if hierarchy[0, index, 3] != specimen_index and hierarchy[0, index, 3] > 0] + # Die Elternkontur für jedes Partikel bestimmen + particle_parents = [hierarchy[0, particle, 3] for particle in particles] + # Anzahl an Partikeln in jeder Partikelpore bestimmen + particles_in_pores = [particle_parents.count(index) for index in particle_pores] + + ## Für jede Pore die Anzahl an Partikeln speichern + pores_particles = [None] * len(pores_index) + for i, index in enumerate(pores_index): + if index in particle_pores: + j = particle_pores.index(index) + pores_particles[i] = particles_in_pores[j] + else: + pores_particles[i] = 0 + + ## für jedes Partikel die Größe bestimmen + particle_size = [cv.contourArea(contours[particle]) for particle in particles] + ## Für jede Pore die Größen der enthaltenen Partikel als Subliste speichern + particle_sizes_pores = [None] * len(pores_index) + for i, index in enumerate(pores_index): + if index in particle_pores: + sizes = [particle_size[i] for i, parent in enumerate(particle_parents) if parent == index] + particle_sizes_pores[i] = sizes + else: + particle_sizes_pores[i] = [0] + ## Statistik zu den Partikelgrößen + max_particle_size_pore = [max(element) if element[0] != 0 else 0 for element in particle_sizes_pores] + min_particle_size_pore = [min(element) if element[0] != 0 else 0 for element in particle_sizes_pores] + mean_particle_size_pore = [sum(element)/len(element) if element[0] != 0 else 0 for element in particle_sizes_pores] + median_particle_size_pore = [np.median(element) if element[0] != 0 else 0 for element in particle_sizes_pores] + std_particle_size_pore = [np.std(element) if element[0] != 0 else 0 for element in particle_sizes_pores] + + ## Porengrößen + pore_areas = [contour_area[index] for index in pores_index] + ## Konturlängen + pore_perimeters = [cv.arcLength(contours[index], True) for index in pores_index] + ## Approximierte Kontur + + ## Konvexitätsfehler + pore_convex_hulls_ret = [cv.convexHull(contours[index], returnPoints=False) for index in pores_index] + pore_convex_hulls = [cv.convexHull(contours[index]) for index in pores_index] + convexity_defects = [cv.convexityDefects(contours[index], pore_convex_hulls_ret[i]) for i, index in enumerate(pores_index)] + pore_convexity_defects = [] + for convexity_defect in convexity_defects: + try: + pore_convexity_defects.append(len(convexity_defect)) + except: + pore_convexity_defects.append(0) + + convexity_defect_sizes = [] + for convexity_defect in convexity_defects: + try: + convexity_defect_sizes.append([defect[0][3] for defect in convexity_defect]) + except: + convexity_defect_sizes.append(0) + + max_convexity_defect = [] + min_convexity_defect = [] + mean_convexity_defect = [] + median_convexity_defect = [] + std_convexity_defect = [] + for convexity_defect in convexity_defect_sizes: + try: + max_convexity_defect.append(max(convexity_defect)) + min_convexity_defect.append(min(convexity_defect)) + mean_convexity_defect.append(sum(convexity_defect)/len(convexity_defect)) + median_convexity_defect.append(np.median(convexity_defect)) + std_convexity_defect.append(np.std(convexity_defect)) + except: + max_convexity_defect.append(0) + min_convexity_defect.append(0) + mean_convexity_defect.append(0) + median_convexity_defect.append(0) + std_convexity_defect.append(0) + + + ## Zirkularität + pore_circularities = [4*math.pi*area/(pore_perimeters[i])**2 for i, area in enumerate(pore_areas)] + + ## Solidität + pore_solidities = [pore_areas[i]/cv.contourArea(convex_hull) for i, convex_hull in enumerate(pore_convex_hulls)] + + ## Bounding Box + pore_bounding = [cv.boundingRect(contours[index]) for index in pores_index] + width_rect = [bounding[2] for bounding in pore_bounding] + height_rect = [bounding[3] for bounding in pore_bounding] + density_bounding_rect = [contour_area[index]/(width_rect[i]*height_rect[i]) for i, index in enumerate(pores_index)] + + ## Bounding Box mit minimaler Fläche + pore_min_area_bounding = [cv.minAreaRect(contours[index]) for index in pores_index] + width_min_rect = [bounding[1][0] for bounding in pore_min_area_bounding] + height_min_rect = [bounding[1][1] for bounding in pore_min_area_bounding] + density_min_rect = [contour_area[index]/(width_min_rect[i]*height_min_rect[i]) for i, index in enumerate(pores_index)] + + ## Porenrotation + pore_rotations = [] + for i, element in enumerate(pore_min_area_bounding): + # element.width < element.heigth + if element[1][0] < element[1][1]: + pore_rotations.append(abs(pore_min_area_bounding[i][2]) + 90) + else: + pore_rotations.append(abs(pore_min_area_bounding[i][2])) + + # Winkel bei Verdrehung der Probe korrigieren + pore_rotations_corr = [angle - cv.minAreaRect(contours[specimen_index])[2] for angle in pore_rotations] + + ## Minimaler einschließender Kreis + min_circles = [cv.minEnclosingCircle(contours[index]) for index in pores_index] + radian_min_circle = [int(min_circle[1]) for min_circle in min_circles] + density_min_circle = [contour_area[index]/(math.pi*radian_min_circle[i]**2) for i, index in enumerate(pores_index)] + + particle_density = np.divide(pores_particles, [get_ContourArea_Microns(area) for area in pore_areas]) + defect_density = np.divide(pore_convexity_defects, [get_ContourArea_Microns(area) for area in pore_areas]) + + ## Mittelpunktkoordinaten auslesen (von min_area_rect) ### + # print(pore_bounding[0]) + x_coordinates = [bounding[0] for bounding in pore_bounding] + y_coordinates = [bounding[1] for bounding in pore_bounding] + + features = {'Pore_Index': pores_index, + 'x_Coordinate': x_coordinates, + 'y_Coordinate': y_coordinates, + 'No_Particles': pores_particles, + 'Particle_Density': particle_density, + 'Max_Particle': [get_ContourArea_Microns(element) for element in max_particle_size_pore] , + 'Min_Particle': [get_ContourArea_Microns(element) for element in min_particle_size_pore], + 'Mean_Particle': [get_ContourArea_Microns(element) for element in mean_particle_size_pore], + 'Median_Particle': [get_ContourArea_Microns(element) for element in median_particle_size_pore], + 'STD_Particle': [get_ContourArea_Microns(element) for element in std_particle_size_pore], + 'Area': [get_ContourArea_Microns(element) for element in pore_areas], + 'Area_PX': pore_areas, + 'Perimeter': [get_Length_Mircons(element) for element in pore_perimeters], + 'Circularity': pore_circularities, + 'Solidity': pore_solidities, + 'Angle': pore_rotations, + 'Angle_corr': pore_rotations_corr, + # 'No_Convexity_Defects': pore_convexity_defects, + 'Defect_Density': defect_density, + 'Max_Convexity_Defect': [get_Length_Mircons(element) for element in max_convexity_defect], + 'Min_Convexity_Defect': [get_Length_Mircons(element) for element in min_convexity_defect], + 'Mean_Convexity_Defect': [get_Length_Mircons(element) for element in mean_convexity_defect], + 'Median_Convexity_Defect': [get_Length_Mircons(element) for element in median_convexity_defect], + 'STD_Convexity_Defect': [get_Length_Mircons(element) for element in std_convexity_defect], + 'Width_Rect': [get_Length_Mircons(element) for element in width_rect], + 'Height_Rect': [get_Length_Mircons(element) for element in height_rect], + 'Width_Min_Rect': [get_Length_Mircons(element) for element in width_min_rect], + 'Height_Min_Rect': [get_Length_Mircons(element) for element in height_min_rect], + 'Density_Rect': density_bounding_rect, + 'Density_min_Rect': density_min_rect, + 'Radius_Circle': [get_Length_Mircons(element) for element in radian_min_circle], + 'Density_min_Circle': density_min_circle, + } + + ## Zuschnitt jeder Pore in Ordner speichern + if save_pores == True: + rois = segment_Contours(bounding_rect=pore_bounding, binary_image=binary_img) + for i, roi in enumerate(rois): + cv.imwrite(save_path+'/'+str(pores_index[i])+'.jpg', roi) + + + return features, binary_img, pores + +##################################################################################################################################################### + +###### Kontur in leeres Bild zeichnen ###### +# Zum Überprüfen, wie die entsprechende Kontur aussieht +def plot_Contour(contours, image, contour_index, bounding_rect, save, crop, name): + ### leeres Bild erzeugen mit der Größe des Eingangsbildes ### + empty_segment = np.zeros_like(image) + + # gewünschte Konturen in einer Konturenliste speichern + # anhand der contour_index Angabe + cnt = [] + [cnt.append(contours[contour]) for i, contour in enumerate(contour_index)] + + # alle Konturen in der erstellten Liste plotten + segment = cv.drawContours(empty_segment, cnt, -1, (255,255,255), -1) + + ### Bild auf relevanten Bereich zuschneiden ### + # nur wenn crop=True, sonst wird Kotur in das Ursprungsbild geplottet + if crop == True: + segment = segment[int(bounding_rect[contour_index][1]): int(bounding_rect[contour_index][1]+int(bounding_rect[contour_index][3])), \ + int(bounding_rect[contour_index][0]): int(bounding_rect[contour_index][0])+int(bounding_rect[contour_index][2])] + + if save == True: + cv.imwrite('Images/segmented_contour_{}.jpg'.format(name), segment) + + fig = plt.figure(figsize=(8,8)) + plt.imshow(segment, cmap='gray') + plt.axis('off') + plt.title('Segmentierte Kontur {}'.format(name)) + plt.show() + +##################################################################################################################################################### + +####### Konturfläche als Mikrometer ausgeben lassen ###### +def get_ContourArea_Microns(area): + one_micron = 1.79173 # Umrechnungsfaktor PX in Mikrometer --> mithilfe der BA Bildanalyse bestimmt + one_sq_micron = one_micron**2 # Umrechnung in Quadratmikrometer + + area_contour_microns = area/one_sq_micron # Umrechnun der Flöche in Quadratmikrometer + + return area_contour_microns + +##################################################################################################################################################### + +###### Längenmaße in Mikrometer umrechnen ###### + +def get_Length_Mircons(data): + one_micron = 1.79173 + data_microns = one_micron * data + + return data_microns + +##################################################################################################################################################### + +###### relative Dichte berechnen ###### +# Berechnung der Dichte aus der Fläche der Probenkontur und der der inneren Poren +# Partikel innerhalb von Poren haben so keine Auswirkung auf die Dichte +# zweiten Dichtewert mit inneren Partikeln bestimmen (ggf. sinnvoll wenn Proben gehipt werden) +def get_relative_Density(specimen_contour, inner_pores): + + ### für die gesamte Probenfläche ### + specimen_area = round( 2000 * 1.79173 )**2 + pores_area = 0 + + for element in inner_pores: + pores_area += cv.contourArea(element) + + rel_density = (specimen_area - pores_area) / specimen_area * 100 + + return rel_density + +##################################################################################################################################################### + +###### Probe in Kern und Randbereich zerlegen ###### +# Ziel ist zwei Binärbilder mit der Ursprungsgröße zu erzeugen +# Dabei ist jeweils einmal die Kontur und einmal der Kern weiß mit schwarzen Poren +# die Binärbilder können anschließend wie das Ursprungsbild durch die Funktionen untersucht werden + +### I N P U T S ### +# contours: alle Konturen die in dem Originalbild gefunden wurden +# contours_index: Indizes aller Poren +# specimen_contour: die spezifische Contour der Probe +# binary_image: das Eingabebild als Binärbild, um die gleiche Bildgröße zu kriegen +# relative_core_size: Die relative Fläche des Kernbereichs zur Boundingbox der Probenflöche +# relative_y_offset: relative Erweiterung des Kernbereichs in y-Richtung +def get_Core_Border(contours, contours_index, specimen_contour, binary_image, relative_core_size, relative_y_offset): + ### Flächenschwerpunkt der Probe finden ### + # Ausgangspunkt für Centercropping + ## Momente der Probenkontur bestimmen ## + moments = cv.moments(specimen_contour) + ## Koordinaten des Zentrums bestimmen ## + x_center = int(moments['m10']/moments['m00']) + y_center = int(moments['m01']/moments['m00']) + + ### Porenkonturen ### + contours_des = [contours[index] for index in contours_index] + + ## Kernbereich segmentieren ## + core_img = get_Core(specimen_contour, relative_core_size, relative_y_offset, binary_image, x_center, y_center, contours_des) + + ## Konturbereich segmentieren ## + border_img = get_Border(specimen_contour, relative_core_size, relative_y_offset, binary_image, x_center, y_center, contours_des) + + return core_img, border_img + + +def get_Core(specimen_contour, relative_core_size, relative_y_offset, binary_image, x_center, y_center, contours_des): + ### Kernbereich segmentieren ### + # Bereich wird relativ zur Boundingbox als rechteckiger Centercrop gewählt + ## Shape von der Probenfläche bestimmen + specimen_boundingRect = cv.boundingRect(specimen_contour) # = (x, y, w, h) + specimen_height = specimen_boundingRect[3] + specimen_width = specimen_boundingRect[2] + ## Zuschnittsgröße festlegen ## + core_width = relative_core_size * specimen_width + core_height = relative_core_size * specimen_height + core_height = core_height+relative_y_offset*core_height + ## Bild zusammenbauen ## + plane_img = np.zeros_like(binary_image) + core_img = plane_img + core_img = cv.rectangle(core_img, (int(x_center-core_width/2), int(y_center-core_height/2)), + (int(x_center+core_width/2), int(y_center+core_height/2)), (255,255,255), -1) + core_img = cv.drawContours(core_img, contours_des, -1, (0,0,0), -1) + core_img = core_img + + return core_img + +def get_Border(specimen_contour, relative_core_size, relative_y_offset, binary_image, x_center, y_center, contours_des): + ## Shape von der Probenfläche bestimmen + specimen_boundingRect = cv.boundingRect(specimen_contour) # = (x, y, w, h) + specimen_height = specimen_boundingRect[3] + specimen_width = specimen_boundingRect[2] + ## Zuschnittsgröße Kern festlegen ## + core_width = relative_core_size * specimen_width + core_height = relative_core_size * specimen_height + core_height = core_height+relative_y_offset*core_height + ## Bild zusammenbauen ## + plane_img = np.zeros_like(binary_image) + border_img = plane_img + border_img = cv.drawContours(border_img, [specimen_contour], 0, (255,255,255), -1) + border_img = cv.drawContours(border_img, contours_des, -1, (0,0,0), -1) + border_img = cv.rectangle(border_img, (int(x_center-core_width/2), int(y_center-core_height/2)), + (int(x_center+core_width/2), int(y_center+core_height/2)), (0,0,0), -1) + + return border_img + +##################################################################################################################################################### + +###### Größen bestimmen ###### +# Ausreißer bzgl. der Porengröße finden +# Einfluss von Ausreißern auf die Dichte/Porosität untersuchen +def get_Sizes(contour): + + ### Flächenberechnungen ### + area_contour = get_ContourArea_Microns(cv.contourArea(contour)) + + return area_contour + +##################################################################################################################################################### + +###### Mittelpunkt(-abstand) und Schwerpunkt(-abstand) ###### +def get_Positions(x_coordinates, y_coordinates, contour_areas): + + ### Mittelpunkt von allen Porenzentren ### + x_mean = sum(x_coordinates) / len(x_coordinates) + y_mean = sum(y_coordinates) / len(y_coordinates) + + ### Mittelpunkt von allen Porenzentren gewichtet mit der Konturflöche ### + x_gravities = [x_coordinates[i]*contour_areas[i] for i in range(len(contour_areas))] + y_gravities = [y_coordinates[i]*contour_areas[i] for i in range(len(contour_areas))] + x_gravity = sum(x_gravities) / sum(contour_areas) + y_gravity = sum(y_gravities) / sum(contour_areas) + + ### Ausgabe der Verschiebung in Prozent ### + x_diff = (x_mean-x_gravity)/x_mean*100 + y_diff = (y_mean-y_gravity)/y_mean*100 + + ### Entfernungen der Porenzentren ### + ## zum Mittelpunkt ## + x_distances = [abs(x_mean-coordinate) for coordinate in x_coordinates] + y_distances = [abs(y_mean-coordinate) for coordinate in y_coordinates] + # Länge der direkten Verbinungslinien + z_distances = [(x**2 + y_distances[i]**2)**(1/2) for i, x in enumerate(x_distances)] + ## zum Schwerpunkt ## + x_distances_gravity = [abs(x_gravity-coordinate) for coordinate in x_coordinates] + y_distances_gravity = [abs(y_gravity-coordinate) for coordinate in y_coordinates] + # Länge der direkten Verbinungslinien + z_distances_gravity = [(x**2 + y_distances_gravity[i]**2)**(1/2) for i, x in enumerate(x_distances_gravity)] + + ### Rückgabe der Positionen und Entfernungen ### + positions = {'center': (x_mean, y_mean), + 'center_of_mass': (x_gravity, y_gravity), + 'center_distances': (x_distances, y_distances), + 'center_of_mass_distances': (x_distances_gravity, y_distances_gravity)} + + return positions + +##################################################################################################################################################### + +###### Statistische Werte einer Liste berechnen ###### +def get_Statistics(data): + no_elements = len(data) + maximum = max(data) + minimum = min(data) + average = sum(data) / len(data) + median = np.median(data) + standard_deviation = np.std(data) + varianz = sum((element-average)**2 for element in data) / len(data) + skew_val = skew(data) + q1 = np.percentile(data, 25) + q3 = np.percentile(data, 75) + # z_score = [(element-average) / standard_deviation for element in data] + unique = len(np.unique(data)) + + ### Dictionary mit allen Werten anlegen ### + statistics = {'NoElements': no_elements, + 'Unique_Elements': unique, + 'Maximum': maximum, + 'Minimum': minimum, + 'Average': average, + 'Median': median, + 'STD': standard_deviation, + 'Varianz': varianz, + 'Skewness': skew_val, + # 'Z_Score': z_score, + 'Q1': q1, + 'Q3': q3} + + return statistics + +##################################################################################################################################################### + +###### Größengewichtete statistische Werte ableiten ###### +# def get_Weighted_Statistics(): + +##################################################################################################################################################### +###### Ausreißer detektieren ###### +def get_Outliers(data): + + ### Interquartilsabstand ### + q1 = np.percentile(data, 25) + q3 = np.percentile(data, 75) + + q_distance = q3-q1 + + iq_high = q3+1.5*q_distance + iq_low = q1-1.5*q_distance + + iq_outliers = [] + for element in data: + if element > iq_high: + iq_outliers.append(element) + if element < iq_low: + iq_outliers.append(element) + + ### Standardabweichung ### + std = np.std(data) + mean = sum(data)/len(data) + + std_high = mean + 3 * std # 99.7 % der Datenpunkte liegen innerhalb von 3 Standardabweichungen --> Ausreißer liegen bei circa 0.3 % + std_low = mean - 3 * std + + std_outliers = [] + for element in data: + if element > std_high: + std_outliers.append(element) + if element < std_low: + std_outliers.append(element) + + ### z-Score ### + z = [(element - mean) / std for element in data] + + z_outliers = [] + for element in data: + if element > 3: + z_outliers.append(element) + if element < -3: + z_outliers.append(element) + + outliers = {'IQ': len(iq_outliers), + 'STD': len(std_outliers), + 'Z': len(z_outliers)} + + return outliers + +##################################################################################################################################################### + +###### Umlaufende Histogramme ###### +# Histogramme von 0° - 180° erzeugen, ggf. nur 0° und 90° +# Bestimmung der statistischen Kenngrößen +# so können Informationen über die lokalität der Poren gewonnen werden + +# Zeilen- und Spaltenweise die Anzahl an schwarzen Pixeln bestimmen --> Welche Zeile/ Spalte hat welchen Schwarzanteil, ggf. relativ umsetzbar +# Statistische Größen aus dem resultierenden Vektor bestimmen +def get_Position_Histograms(binary_image): + img_height = binary_image.shape[0] + img_width = binary_image.shape[1] + + vertical_histogram = np.sum(binary_image==255, axis=1).tolist()#[np.sum(binary_image[row]==0) for row in range(img_height)] + + # print('Bildhöhe: {}, Länge vertikales Histogramm: {}'.format(img_height, len(vertical_histogram))) + + horizontal_histogram = np.sum(binary_image==255, axis=0).tolist() + + # print('Bildbreite: {}, Länge horizontales Histogramm: {}'.format(img_width, len(horizontal_histogram))) + + return vertical_histogram, horizontal_histogram + +##################################################################################################################################################### + +###### Repräsentative Pore erzeugen ###### +# innere Poren aus originalem Binärbild ausschneiden +# Größten Zuschnitt ermitteln und Bild aus [None]s erzeugen +# segmentierte Poren ggf. invertieren +# Poren in weiß (255) in das None einzeichnen +# Bild an Funktion zum überlagern übergeben +# alle Porenbilder überlagern +## ! https://stackoverflow.com/questions/17291455/how-to-get-an-average-picture-from-100-pictures-using-pil ! ## + +# Primär sinnvoll für alle großen Poren (z.B. die Ausreißer die wesentlich Größer sind) +def get_Average_Pore(segmented_Contours, contour_Index, threshold, plot): + + ### nur die Contouren der inneren Poren segmentieren ### + contours_des = [] + [contours_des.append(segmented_Contours[index]) for index in contour_Index] + + ### Breiten und Höhen der Boundingboxen aller Poren bestimmen ### + widths = [] + [widths.append(contours_des[i].shape[1]) for i in range(len(contours_des))] + heights = [] + [heights.append(contours_des[i].shape[0]) for i in range(len(contours_des))] + + ### leeres Bild erzeugen ### + shape = np.ones([max(heights), max(widths)]) * 255 + avr_img = np.zeros([max(heights), max(widths)]) + + ### Mittlere Pixelintensität bestimmen ### + for contour in contours_des: + scaled = np.ones_like(shape)*255 # leere Skalierungsmatrix erzeugen + + ## Fehlende Zeilen und Spalten bestimmen ## + missing_left = int((shape.shape[1] - contour.shape[1]) / 2) + missing_up = int((shape.shape[0] - contour.shape[0]) / 2) + + ## Werte der Contour in scaled eintragen ## + # Start (oben links) in shape ist [missing_up-1, missing_left-1] + # len(contour) = Zeilen/Höhe + # len(contour[0]) = Spalten/Breite + for i in range(len(contour)): + for j in range(len(contour[0])): + scaled[missing_up-1+i][missing_left-1+j] = contour[i][j] + + ## Mittelwertbild erzeugen ## + avr_img = avr_img + scaled/len(contours_des) + + ### Mittelwertbild binarisieren ### + _, avr_img = cv.threshold(avr_img, threshold, 255, cv.THRESH_BINARY) + + ### Average Pore plotten ### + if plot == True: + plt.imshow(avr_img, cmap='gray') + plt.axis('off') + plt.title('Average Pore') + plt.show() + + ### Mittelwertbild zurückgeben ### + return avr_img + +##################################################################################################################################################### + +###### Repräsentative Pore erzeugen mit Gewichtung der Porengröße in die Überlagerung ###### +def get_Average_Pore_weighted(contours, segmented_Contours, contour_Index, threshold, plot): + + if contour_Index != -1: + ### nur die Contouren der inneren Poren segmentieren ### + contours_des = [] + [contours_des.append(segmented_Contours[index]) for index in contour_Index] + contours_des_area = [contours[index] for index in contour_Index] + contours_area = [cv.contourArea(contour) for contour in contours_des_area] + else: + contours_des = segmented_Contours + contours_area = [cv.contourArea(contour) for contour in contours] + + ### Breiten und Höhen der Boundingboxen aller Poren bestimmen ### + widths = [] + [widths.append(contours_des[i].shape[1]) for i in range(len(contours_des))] + heights = [] + [heights.append(contours_des[i].shape[0]) for i in range(len(contours_des))] + + ### leeres Bild erzeugen ### + shape = np.ones([max(heights), max(widths)]) * 255 + avr_img = np.zeros([max(heights), max(widths)]) + + + ### Mittlere Pixelintensität bestimmen ### + for index, contour in enumerate(contours_des): + scaled = np.ones_like(shape)*255 # leere Skalierungsmatrix erzeugen + + ## Fehlende Zeilen und Spalten bestimmen ## + missing_left = int((shape.shape[1] - contour.shape[1]) / 2) + missing_up = int((shape.shape[0] - contour.shape[0]) / 2) + + ## Werte der Contour in scaled eintragen ## + # Start (oben links) in shape ist [missing_up-1, missing_left-1] + # len(contour) = Zeilen/Höhe + # len(contour[0]) = Spalten/Breite + for i in range(len(contour)): + for j in range(len(contour[0])): + scaled[missing_up-1+i][missing_left-1+j] = contour[i][j] + + ## Mittelwertbild erzeugen ## + avr_img = avr_img + scaled * contours_area[index] / sum(contours_area) + + ### Mittelwertbild binarisieren ### + _, avr_img = cv.threshold(avr_img, threshold, 255, cv.THRESH_BINARY) + + ### Average Pore plotten ### + if plot == True: + plt.imshow(avr_img, cmap='gray') + plt.axis('off') + plt.title('Average Pore weighted by Poresize') + plt.show() + + ### Mittelwertbild zurückgeben ### + return avr_img + +##################################################################################################################################################### + +###### lokale Dichte bestimmen ###### +# Zerlegung des Bildes in Quadrate +# Konturen in Quadrat bestimmen +# Anzahl an Poren, Form, Typ und Größe bestimmen +# relative Dichte im Quadrat bestimmen + +##################################################################################################################################################### \ No newline at end of file diff --git a/analyzer/features.py b/analyzer/features.py new file mode 100644 index 0000000..e5321c9 --- /dev/null +++ b/analyzer/features.py @@ -0,0 +1,440 @@ +######################################################## F E A T U R E E X T R A K T I O N ######################################################## +###### ###### +###### A U T H O R I N F O R M A T I O N S ###### +###### Mika Leon Altmann ###### +###### 31th of March, 2023 ###### +###### Leibniz-Institute for Materials Science, Bremen, Germany ###### +###### ###### +###### D E S C R I B T I O N ###### +###### Feature exraction of micrographs for powder bed fusion with laser beam of metals. ###### +###### ###### +##################################################################################################################################################### + +import cv2 as cv +import numpy as np +from numpy.linalg import norm +from scipy.stats import skew +import pandas as pd +from heatmap import heatmap, corrplot + +from sklearn.neighbors import NearestNeighbors +from kneed import KneeLocator +from sklearn.cluster import DBSCAN +from sklearn import metrics +from sklearn.decomposition import PCA +from mpl_toolkits import mplot3d +from mpl_toolkits.mplot3d import axes3d +from sklearn.preprocessing import QuantileTransformer + +import matplotlib.pyplot as plt +import os + + +# Klasse zur Beschreibung der Porenfeatures (lokale Features) wie der Größe, Form und Position +class Pore(): + def __init__(self, contour, scale=1.79173): + self.contour = contour + self.scale = scale + self.area = self.__area() + self.perimeter = self.__perimeter() + self.convex_hull = self.__convex_hull() + self.convexity_defects = self.__convexity_defects() + self.defect_density = self.__defect_density() + self.mean_defect = self.__mean_defect() + self.solidity = self.__solidity() + self.bounding_box = self.__bounding_box() + self.x = self.bounding_box[0] + self.y = self.bounding_box[1] + self.label = None + + def __area(self): + area = cv.contourArea(self.contour) + return area / (self.scale**2) + + def __perimeter(self): + return cv.arcLength(self.contour, True) / self.scale + + def __convex_hull(self): + convex_hull_ret = cv.convexHull(self.contour, returnPoints=False) # returning indices of the contour points making the convex hull + convex_hull = cv.convexHull(self.contour) # returning the coordinates of the point making the convex hull + return convex_hull_ret, convex_hull + + def __convexity_defects(self): + convexity_defects = cv.convexityDefects(self.contour, self.convex_hull[0]) + return convexity_defects + + def __defect_density(self): + try: + defects = len(self.convexity_defects) + except: + defects = 0 + + defect_density = defects / self.area * 100 + return defect_density + + def __mean_defect(self): + # print('pore') + # print(self.convexity_defects) + try: + defects = self.__defect_size() + except: + defects = [0] + + mean_defect = sum(defects) / len(defects) + return mean_defect / self.scale + + def __defect_size(self): # Berechnung der Größe der Konvexitätsfehler --> Bug in der openCV Berechnung --> Faktor 255 größer als wirklich + s = [ dfct[0][0] for dfct in self.convexity_defects ] + e = [ dfct[0][1] for dfct in self.convexity_defects ] + d = [ dfct[0][2] for dfct in self.convexity_defects ] + distance = [ norm(np.cross(self.contour[s[idx]][0]-self.contour[e[idx]][0], self.contour[e[idx]][0] - self.contour[d[idx]][0])/norm(self.contour[s[idx]][0]-self.contour[e[idx]][0])) for idx in range(len(d)) ] + return distance + + + def __solidity(self): + solidity = self.area / (cv.contourArea(self.convex_hull[1])/self.scale**2) + return solidity + + def __bounding_box(self): + bounding_box = cv.boundingRect(self.contour) + return bounding_box + + def set_label(self, label): + self.label = label + + +# Klasse zur allgemeinen Beschreibung eines Schliffbildes, Zuschnitt, Binarisierung, Skalierung und Poren +class MicrographBase(): + def __init__(self, image, scale=1.79173, cropsize_microns=2000): + self.img = image + self.scl = scale + self.crpsz = cropsize_microns + self.bnry = self.__binary() + self.cnt_crp = self.__center_crop() + self.cnt_hrrchy = self.__contours_hierarchy() + self.prs = self.__pores() + self.img_cnt = self.__img_contour() + + def __binary(self): + try: + image = cv.cvtColor(self.img, cv.COLOR_RGB2GRAY) + except: + image = self.img + + img_blur = cv.GaussianBlur(image, (5, 5), 0) + threshold, img_binary = cv.threshold(img_blur, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU) + return img_binary + + def __center_crop(self): + height = self.bnry.shape[0] + width = self.bnry.shape[1] + + w = round( self.crpsz * self.scl ) + h = round( self.crpsz * self.scl ) + + x = round( width/2 - w/2 ) + y = round( height/2 - h/2 ) + + binary_img = self.bnry[y:y+h, x:x+w] + binary_img = cv.copyMakeBorder(binary_img, 1, 1, 1, 1, cv.BORDER_CONSTANT, None, value=255) + binary_img = cv.copyMakeBorder(binary_img, 1, 1, 1, 1, cv.BORDER_CONSTANT, None, value=0) + return binary_img + + def __contours_hierarchy(self): + cnt, hrrchy = cv.findContours(self.cnt_crp, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE) + return cnt, hrrchy[0] + + def __pores(self): + # Überprüfung ob die gefundenen Konturen die Probenkontur als Elternkontur haben --> nur dann Pore + prs_cnts = [ cnt for idx, cnt in enumerate(self.cnt_hrrchy[0]) if self.cnt_hrrchy[1][idx][3] == 0 and idx != 0 ] + prs = [] + for cnt in prs_cnts: + prs.append(Pore(cnt, self.scl)) + return prs + + def __img_contour(self): + img_cnt = cv.cvtColor(self.cnt_crp, cv.COLOR_GRAY2RGB) + for idx, pore in enumerate(self.prs): + # x, y = tuple(cnt[cnt[:, :, 0].argmin()][0]) + x = pore.x + y = pore.y + img_cnt = cv.putText(img_cnt, str(idx), (x, y-round(0.01*self.crpsz)), cv.FONT_HERSHEY_SIMPLEX, 0.45, 255, 1) + return img_cnt + + def set_center_crop(self, crop_size_microns=2000): # Zuschnittsgröße von außen verändern + self.crpsz = crop_size_microns + self.cnt_crp = self.__center_crop() + + def save_pore_segments(self, path=os.getcwd()): + path=path+'\Pores' + if not os.path.exists(path): + os.makedirs(path) + binary_image = self.cnt_crp + bounding_rect = [pore.bounding_box for pore in self.prs] + rois = [None]*len(bounding_rect) + + # Über alle Boundingboxen iterieren und die ROIs in dem Array speichern + for i, rect in enumerate(bounding_rect): + rois[i] = binary_image[int(bounding_rect[i][1]): int(bounding_rect[i][1]+int(bounding_rect[i][3])), + int(bounding_rect[i][0]): int(bounding_rect[i][0])+int(bounding_rect[i][2])] + + for i, roi in enumerate(rois): + cv.imwrite(path+'/'+str(i)+'.jpg', roi) + + print('Saved segmented pores. \n Path: {}'.format(path)) + + +# Klasse zur Beschreibung der statistischen Kenngrößen in einem Schliffbild, bzgl. der Poren +class Micrograph(MicrographBase): + def __init__(self, image, scale=1.79173, cropsize_microns=2000): + super().__init__(image, scale, cropsize_microns) + self.rltv_dnsty = self.__relative_density() + self.pr_dnsty = self.__pore_density() + self.sldty = self.calc_stats([pore.solidity for pore in self.prs]) + self.area = self.calc_stats([pore.area for pore in self.prs]) + self.prmtr = self.calc_stats([pore.perimeter for pore in self.prs]) + self.dfct_dnsty = self.calc_stats([pore.defect_density for pore in self.prs]) + self.mn_dfct = self.calc_stats([pore.mean_defect for pore in self.prs]) + + def __relative_density(self): + area_mcrgrph = self.crpsz**2 + area_pr = sum( [ pr.area for pr in self.prs ] ) + rltv_dnsty = 100 - area_pr / area_mcrgrph * 100 + return rltv_dnsty + + def __pore_density(self): + nbr_prs = len(self.prs) + pr_dnsty = nbr_prs / (self.crpsz**2) * 100 + return pr_dnsty + + def calc_stats(self, lst): + stts = {'Count': len(lst), + 'Unique': len(np.unique(lst)), + 'Max': max(lst), + 'Min': min(lst), + 'Mean': sum(lst) / len(lst), + 'Median': np.median(lst), + 'Std_Dev': np.std(lst), + 'Varianz': sum((elmnt-(sum(lst)/len(lst)))**2 for elmnt in lst) / len(lst), + 'Skewness': skew(lst)} + return stts + + def stats(self): + lst = [self.sldty, self.area, self.prmtr, self.dfct_dnsty, self.mn_dfct] + stats = pd.DataFrame.from_records(lst, index=['Solidity', 'Area', 'Perimeter', 'Defect Density', 'Mean Defect']).round(decimals=3) + return stats + + + +# Klasse zur Beschreibung der Schliffbilder mit Identifizierung der typischen und atypischen Poren +class MicrographDBSCAN(Micrograph): + def __init__(self, image, scale=1.79173, cropsize_microns=2000, n_neighbors=2, min_samples = 3): + super().__init__(image, scale, cropsize_microns) + self.n_neighbors = ( n_neighbors if n_neighbors <= len(self.prs) else len(self.prs)) + self.min_samples = min_samples + self.pore_features = self.__pore_features() + self.pore_ftrs_nrmlzd = self.__normalization() + self.knee = self.__calc_knee() + self.pca = self.__pca() + self.pores_dbscan = self.__dbscan() + + def __pore_features(self): + ftrs = {'Area': [ pore.area for pore in self.prs ], + 'Solidity': [ pore.solidity for pore in self.prs ], + 'Perimeter': [ pore.perimeter for pore in self.prs ], + 'Defect_Density': [ pore.defect_density for pore in self.prs ], + 'Mean_Defect': [ pore.mean_defect for pore in self.prs ]} + + pores_ftrs = pd.DataFrame.from_dict(ftrs) + return pores_ftrs + + def __normalization(self): + pores_ftrs = self.pore_features.copy() + cols = pores_ftrs.columns.tolist() + ftrs = pores_ftrs[cols] + + sclr = QuantileTransformer(n_quantiles = pores_ftrs.shape[0]) + pores_ftrs[cols] = sclr.fit_transform(ftrs.values) + return pores_ftrs + + def __pca(self): + if len(self.prs) < 5: + raise CustomException('Less than 5 pores found, unable to calculate stats.') + pca_ = PCA(n_components=len(self.pore_ftrs_nrmlzd.columns.tolist()), random_state=2020) + pca_.fit(self.pore_ftrs_nrmlzd) + explanation = np.cumsum(pca_.explained_variance_ratio_ * 100) + + pca3 = PCA(n_components=3, random_state=2020) + pca3.fit(self.pore_ftrs_nrmlzd) + pores_pca3 = pca3.transform(self.pore_ftrs_nrmlzd) + return explanation, pores_pca3 + + def __calc_knee(self): + # finding best epsilon for DBSCAN: https://iopscience.iop.org/article/10.1088/1755-1315/31/1/012012/pdf + nbrs = NearestNeighbors(n_neighbors = self.n_neighbors, metric='euclidean').fit(self.pore_ftrs_nrmlzd) # Anzahl an Nachbarn sollte etwa das Doppelte von der Featureanzahl sein + neigh_dist, neigh_ind = nbrs.kneighbors(self.pore_ftrs_nrmlzd) + sort_neigh_dist = np.sort(neigh_dist, axis=0) + + k_dist = sort_neigh_dist[:, self.n_neighbors-1] + x = [i for i in range(len(k_dist))] + + kneedle = KneeLocator(x=x, y=k_dist, S=1.0, curve='concave', direction='increasing', online=True) + knee = kneedle.knee_y + return knee + + def __dbscan(self): + knee = ( self.knee if self.knee != None else 0.1 ) + db = DBSCAN(eps=knee, min_samples=self.min_samples).fit(self.pore_ftrs_nrmlzd) + labels = db.labels_ + + i = 0 + for element in labels: # Anzahl an Ausreißern zählen + if element==-1: + i+=1 + + if i > 0.1*len(labels): # Wenn Ausreißer Anzahl mehr als 10 % der Poren sind, alle Ausreißer zu Klasse 0 zuweisen + for i in range(len(labels)): + labels[i] = 0 + + for i, pore in enumerate(self.prs): # Label zu jeder Pore speichern + pore.set_label(labels[i]) + + n_clusters = len(set(labels)) - (1 if -1 in labels else 0) + n_noise = list(labels).count(-1) + + return n_clusters, n_noise + + def explain_pca(self): + print('The varianz of the pores captured by the principle components: \n 1 principle component: {:.3f} % \n 2 principle components: {:.3f} % \n 3 principle components: {:.3f} % \n 4 principle components: {:.3f} % \n 5 principle components: {:.3f} %'.format(self.pca[0][0], self.pca[0][1],self.pca[0][2], self.pca[0][3], self.pca[0][4])) + + def visualize_clusters(self): + labels = [pore.label for pore in self.prs] + + fig = plt.figure(figsize=(12,8)) + ax = plt.axes(projection="3d") + + sctt = ax.scatter3D(self.pca[1][:,0], self.pca[1][:,1], self.pca[1][:,2], c=labels, s=25, alpha=0.6, cmap='viridis') + + plt.title('3D Scatterplot: {:.2f} % of the variability captured'.format(self.pca[0][2])) + ax.set_xlabel('PC 1', labelpad=15, weight='bold') + ax.set_ylabel('PC 2', labelpad=10, weight='bold') + ax.set_zlabel('PC 3', labelpad=10, weight='bold') + ax.view_init(25, 10) + return fig + + def hist_pore_features(self, normalized=False, include_outliers=True): + if normalized == False: + data = self.__pore_features() + data['Label'] = [pore.label for pore in self.prs] + if include_outliers == False: + data = data[data['Label'] >= 0] + + else: + data = self.__normalization() + data['Label'] = [pore.label for pore in self.prs] + if include_outliers == False: + data = data[data['Label'] >= 0] + + data.hist(bins=30, figsize=(8,8)) + + def get_corr_plot(self, include_outliers=True): + data = self.__pore_features() + data['Label'] = [pore.label for pore in self.prs] + if include_outliers==True: + data = data + else: + data = data[data['Label'] >= 0] + + fig = plt.figure(figsize=(16,8)) + corr = data.corr(numeric_only=True) + corrplot(corr, size_scale=100, marker="s") + + def get_stats(self, include_outliers=True): + if include_outliers==True: + lst = [self.sldty, self.area, self.prmtr, self.dfct_dnsty, self.mn_dfct] + else: + solidity = self.calc_stats([pore.solidity for i, pore in enumerate(self.prs) if pore.label >= 0]) + area = self.calc_stats([pore.area for i, pore in enumerate(self.prs) if pore.label >= 0]) + perimeter = self.calc_stats([pore.perimeter for i, pore in enumerate(self.prs) if pore.label >= 0]) + defect_density = self.calc_stats([pore.defect_density for i, pore in enumerate(self.prs) if pore.label >= 0]) + mean_defect = self.calc_stats([pore.mean_defect for i, pore in enumerate(self.prs) if pore.label >= 0]) + + lst = [solidity, area, perimeter, defect_density, mean_defect] + + stats = pd.DataFrame.from_records(lst, index=['Solidity', 'Area', 'Perimeter', 'Defect Density', 'Mean Defect']).round(decimals=3) + return stats + + +class MicrographFullDescription(MicrographDBSCAN): #Ausreißer ein- oder ausschließen + def __init__(self, image, laserpower, scanspeed, hatchdistance, layerthickness, scale=1.79173, cropsize_microns=2000, n_neighbors=2, min_samples = 3): + super().__init__(image, scale, cropsize_microns, n_neighbors, min_samples) + self.laserpower = laserpower + self.scanspeed = scanspeed + self.hatchdistance = hatchdistance + self.layerthickness = layerthickness + self.global_ftrs = self.__global_ftrs() + self.global_stts = self.__global_stts() + self.local_ftrs = self.__local_frts() + + def __global_ftrs(self): + lst = {'LaserPower': self.laserpower, + 'ScanSpeed': self.scanspeed, + 'HatchDistance': self.hatchdistance, + 'LayerThickness': self.layerthickness, + 'RelativeDensity': self.rltv_dnsty, + 'PoreDensity': self.pr_dnsty, + 'ED': self.laserpower/(self.scanspeed*self.hatchdistance*self.layerthickness), + 'TE': self.laserpower/self.scanspeed, + 'CountOutliers': self.pores_dbscan[1], + 'CountClusters': self.pores_dbscan[0], + 'EpsilonDBSCAN': self.knee} + global_ftrs = pd.DataFrame(data=lst, index=[0]) + return global_ftrs + + def __local_frts(self): + local_ftrs = self.pore_features + local_ftrs['Label'] = [pore.label for pore in self.prs] + return local_ftrs + + def __global_stts(self): + keys = self.area.keys() + area = {} + solidity = {} + perimeter = {} + defect_density = {} + mean_defect = {} + + for key in keys: + area['Area'+key] = [self.area[key]] + solidity['Solidity'+key] = [self.sldty[key]] + perimeter['Perimeter'+key] = [self.prmtr[key]] + defect_density['DefectDensity'+key] = [self.dfct_dnsty[key]] + mean_defect['MeanDefect'+key] = [self.mn_dfct[key]] + + global_stts = pd.DataFrame.from_dict(area) + lst = [solidity, perimeter, defect_density, mean_defect] + for elmnt in lst: + global_stts = pd.concat([global_stts, pd.DataFrame.from_dict(elmnt)], axis=1) + + return global_stts + + def get_full_description(self, include_stats=True, include_pore_features=True): + if include_stats == True: + if include_pore_features == True: + data = pd.concat([self.local_ftrs, self.global_ftrs, self.global_stts], axis=1) + else: + data = pd.concat([self.global_ftrs, self.global_stts], axis=1) + else: + if include_pore_features == True: + data = pd.concat([self.local_ftrs, self.global_ftrs], axis=1) + else: + data = pd.concat([self.global_ftrs], axis=1) + + nans = data.isna().any().tolist() + cols = data.columns.tolist() + + for i, col in enumerate(cols): + if nans[i] == True: + data[col] = data[col][0] + + return data \ No newline at end of file