From d3110caac2c43bea5414265aafa68a989592bdd2 Mon Sep 17 00:00:00 2001 From: Paul G <paul.graness@stud.hs-hannover.de> Date: Fri, 28 Jul 2023 18:34:57 +0200 Subject: [PATCH] init --- .gitignore | 2 + delete_images.ipynb | 61 +++++++ detecting_anomalie.ipynb | 379 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 85 +++++++++ 4 files changed, 527 insertions(+) create mode 100644 .gitignore create mode 100644 delete_images.ipynb create mode 100644 detecting_anomalie.ipynb create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de54d64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv +.venv\Lib\site-packages \ No newline at end of file diff --git a/delete_images.ipynb b/delete_images.ipynb new file mode 100644 index 0000000..40eaf83 --- /dev/null +++ b/delete_images.ipynb @@ -0,0 +1,61 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import random\n", + "\n", + "# Pfad zum Ordner, in dem sich die zu löschenden Dateien befinden\n", + "folder_path = \"D:/Studium/Masterarbeit/Einarbeitung/Codebeispiele/detecting_anomalies/data/cell_images/parasitized\"\n", + "\n", + "# Liste der Dateinamen im Ordner\n", + "file_list = os.listdir(folder_path)\n", + "\n", + "# Anzahl der Dateien, die Sie löschen möchten\n", + "num_files_to_delete = 0\n", + "\n", + "# Zufällige Auswahl der Dateien zum Löschen\n", + "files_to_delete = random.sample(file_list, num_files_to_delete)\n", + "\n", + "# Schleife zum Löschen der ausgewählten Dateien\n", + "for file_name in files_to_delete:\n", + " file_path = os.path.join(folder_path, file_name)\n", + " os.remove(file_path)\n", + " print(f\"Datei {file_name} wurde gelöscht.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.4" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/detecting_anomalie.ipynb b/detecting_anomalie.ipynb new file mode 100644 index 0000000..cf975a2 --- /dev/null +++ b/detecting_anomalie.ipynb @@ -0,0 +1,379 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quelle\n", + "https://github.com/bnsreenu/python_for_microscopists/blob/master/260_image_anomaly_detection_using_autoencoders/260_image_anomaly_detection_using_autoencoders.py\n", + "\n", + "``Infos``\\\n", + "Detecting anomaly images using AutoEncoders. (Sorting an entire image as either normal or anomaly)\\\n", + "Here, we use both the reconstruction error and also the kernel density estimation based on the vectors in the latent space. \n", + "We will consider the bottleneck layer outputfrom our autoencoder as the latent space.\\\n", + "This code uses the malarial data set but it can be easily applied to any application. \n", + "\n", + "Data from: https://data.lhncbc.nlm.nih.gov/public/Malaria/cell_images.zip" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from tensorflow.keras.models import Sequential\n", + "from tensorflow.keras.layers import Conv2D, MaxPooling2D, UpSampling2D\n", + "from tensorflow.keras.preprocessing.image import ImageDataGenerator\n", + "\n", + "from PIL import Image\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import random" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 0 images belonging to 0 classes.\n" + ] + }, + { + "data": { + "text/plain": [ + "\"\\nvalidation_generator = datagen.flow_from_directory(\\n 'data/cell_images/uninfected_test/',\\n target_size=(SIZE, SIZE),\\n batch_size=batch_size,\\n class_mode='input'\\n )\\n\\nanomaly_generator = datagen.flow_from_directory(\\n 'data/cell_images/parasitized/',\\n target_size=(SIZE, SIZE),\\n batch_size=batch_size,\\n class_mode='input'\\n )\\n\"" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#Size of our input images\n", + "SIZE = 128\n", + "\n", + "#Define generators for training, validation and also anomaly data.\n", + "batch_size = 64\n", + "datagen = ImageDataGenerator(rescale=1./255)\n", + "\n", + "train_generator = datagen.flow_from_directory(\n", + " 'data/cell_images/uninfected_train/',\n", + " target_size=(SIZE, SIZE),\n", + " batch_size=batch_size,\n", + " class_mode='input'\n", + " )\n", + "'''\n", + "validation_generator = datagen.flow_from_directory(\n", + " 'data/cell_images/uninfected_test/',\n", + " target_size=(SIZE, SIZE),\n", + " batch_size=batch_size,\n", + " class_mode='input'\n", + " )\n", + "\n", + "anomaly_generator = datagen.flow_from_directory(\n", + " 'data/cell_images/parasitized/',\n", + " target_size=(SIZE, SIZE),\n", + " batch_size=batch_size,\n", + " class_mode='input'\n", + " )\n", + "'''" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model: \"sequential\"\n", + "_________________________________________________________________\n", + " Layer (type) Output Shape Param # \n", + "=================================================================\n", + " conv2d (Conv2D) (None, 128, 128, 64) 1792 \n", + " \n", + " max_pooling2d (MaxPooling2 (None, 64, 64, 64) 0 \n", + " D) \n", + " \n", + " conv2d_1 (Conv2D) (None, 64, 64, 32) 18464 \n", + " \n", + " max_pooling2d_1 (MaxPoolin (None, 32, 32, 32) 0 \n", + " g2D) \n", + " \n", + " conv2d_2 (Conv2D) (None, 32, 32, 16) 4624 \n", + " \n", + " max_pooling2d_2 (MaxPoolin (None, 16, 16, 16) 0 \n", + " g2D) \n", + " \n", + " conv2d_3 (Conv2D) (None, 16, 16, 16) 2320 \n", + " \n", + " up_sampling2d (UpSampling2 (None, 32, 32, 16) 0 \n", + " D) \n", + " \n", + " conv2d_4 (Conv2D) (None, 32, 32, 32) 4640 \n", + " \n", + " up_sampling2d_1 (UpSamplin (None, 64, 64, 32) 0 \n", + " g2D) \n", + " \n", + " conv2d_5 (Conv2D) (None, 64, 64, 64) 18496 \n", + " \n", + " up_sampling2d_2 (UpSamplin (None, 128, 128, 64) 0 \n", + " g2D) \n", + " \n", + " conv2d_6 (Conv2D) (None, 128, 128, 3) 1731 \n", + " \n", + "=================================================================\n", + "Total params: 52067 (203.39 KB)\n", + "Trainable params: 52067 (203.39 KB)\n", + "Non-trainable params: 0 (0.00 Byte)\n", + "_________________________________________________________________\n" + ] + }, + { + "ename": "ValueError", + "evalue": "Asked to retrieve element 0, but the Sequence has length 0", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[7], line 28\u001b[0m\n\u001b[0;32m 25\u001b[0m model\u001b[39m.\u001b[39msummary()\n\u001b[0;32m 27\u001b[0m \u001b[39m#Fit the model. \u001b[39;00m\n\u001b[1;32m---> 28\u001b[0m history \u001b[39m=\u001b[39m model\u001b[39m.\u001b[39;49mfit(\n\u001b[0;32m 29\u001b[0m train_generator,\n\u001b[0;32m 30\u001b[0m steps_per_epoch\u001b[39m=\u001b[39;49m \u001b[39m500\u001b[39;49m \u001b[39m/\u001b[39;49m\u001b[39m/\u001b[39;49m batch_size,\n\u001b[0;32m 31\u001b[0m epochs\u001b[39m=\u001b[39;49m\u001b[39m1000\u001b[39;49m,\n\u001b[0;32m 32\u001b[0m validation_data\u001b[39m=\u001b[39;49mvalidation_generator,\n\u001b[0;32m 33\u001b[0m validation_steps\u001b[39m=\u001b[39;49m\u001b[39m75\u001b[39;49m \u001b[39m/\u001b[39;49m\u001b[39m/\u001b[39;49m batch_size,\n\u001b[0;32m 34\u001b[0m shuffle \u001b[39m=\u001b[39;49m \u001b[39mTrue\u001b[39;49;00m)\n\u001b[0;32m 37\u001b[0m \u001b[39m#plot the training and validation accuracy and loss at each epoch\u001b[39;00m\n\u001b[0;32m 38\u001b[0m loss \u001b[39m=\u001b[39m history\u001b[39m.\u001b[39mhistory[\u001b[39m'\u001b[39m\u001b[39mloss\u001b[39m\u001b[39m'\u001b[39m]\n", + "File \u001b[1;32md:\\Studium\\Masterarbeit\\Einarbeitung\\Codebeispiele\\detecting_anomalies\\.venv\\Lib\\site-packages\\keras\\src\\utils\\traceback_utils.py:70\u001b[0m, in \u001b[0;36mfilter_traceback.<locals>.error_handler\u001b[1;34m(*args, **kwargs)\u001b[0m\n\u001b[0;32m 67\u001b[0m filtered_tb \u001b[39m=\u001b[39m _process_traceback_frames(e\u001b[39m.\u001b[39m__traceback__)\n\u001b[0;32m 68\u001b[0m \u001b[39m# To get the full stack trace, call:\u001b[39;00m\n\u001b[0;32m 69\u001b[0m \u001b[39m# `tf.debugging.disable_traceback_filtering()`\u001b[39;00m\n\u001b[1;32m---> 70\u001b[0m \u001b[39mraise\u001b[39;00m e\u001b[39m.\u001b[39mwith_traceback(filtered_tb) \u001b[39mfrom\u001b[39;00m \u001b[39mNone\u001b[39;00m\n\u001b[0;32m 71\u001b[0m \u001b[39mfinally\u001b[39;00m:\n\u001b[0;32m 72\u001b[0m \u001b[39mdel\u001b[39;00m filtered_tb\n", + "File \u001b[1;32md:\\Studium\\Masterarbeit\\Einarbeitung\\Codebeispiele\\detecting_anomalies\\.venv\\Lib\\site-packages\\keras\\src\\preprocessing\\image.py:103\u001b[0m, in \u001b[0;36mIterator.__getitem__\u001b[1;34m(self, idx)\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39m__getitem__\u001b[39m(\u001b[39mself\u001b[39m, idx):\n\u001b[0;32m 102\u001b[0m \u001b[39mif\u001b[39;00m idx \u001b[39m>\u001b[39m\u001b[39m=\u001b[39m \u001b[39mlen\u001b[39m(\u001b[39mself\u001b[39m):\n\u001b[1;32m--> 103\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mValueError\u001b[39;00m(\n\u001b[0;32m 104\u001b[0m \u001b[39m\"\u001b[39m\u001b[39mAsked to retrieve element \u001b[39m\u001b[39m{idx}\u001b[39;00m\u001b[39m, \u001b[39m\u001b[39m\"\u001b[39m\n\u001b[0;32m 105\u001b[0m \u001b[39m\"\u001b[39m\u001b[39mbut the Sequence \u001b[39m\u001b[39m\"\u001b[39m\n\u001b[0;32m 106\u001b[0m \u001b[39m\"\u001b[39m\u001b[39mhas length \u001b[39m\u001b[39m{length}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(idx\u001b[39m=\u001b[39midx, length\u001b[39m=\u001b[39m\u001b[39mlen\u001b[39m(\u001b[39mself\u001b[39m))\n\u001b[0;32m 107\u001b[0m )\n\u001b[0;32m 108\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mseed \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m 109\u001b[0m np\u001b[39m.\u001b[39mrandom\u001b[39m.\u001b[39mseed(\u001b[39mself\u001b[39m\u001b[39m.\u001b[39mseed \u001b[39m+\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mtotal_batches_seen)\n", + "\u001b[1;31mValueError\u001b[0m: Asked to retrieve element 0, but the Sequence has length 0" + ] + } + ], + "source": [ + "#Define the autoencoder. \n", + "#Try to make the bottleneck layer size as small as possible to make it easy for\n", + "#density calculations and also picking appropriate thresholds. \n", + "\n", + "#Encoder\n", + "model = Sequential()\n", + "model.add(Conv2D(64, (3, 3), activation='relu', padding='same', input_shape=(SIZE, SIZE, 3)))\n", + "model.add(MaxPooling2D((2, 2), padding='same'))\n", + "model.add(Conv2D(32, (3, 3), activation='relu', padding='same'))\n", + "model.add(MaxPooling2D((2, 2), padding='same'))\n", + "model.add(Conv2D(16, (3, 3), activation='relu', padding='same'))\n", + "model.add(MaxPooling2D((2, 2), padding='same'))\n", + "\n", + "#Decoder\n", + "model.add(Conv2D(16, (3, 3), activation='relu', padding='same'))\n", + "model.add(UpSampling2D((2, 2)))\n", + "model.add(Conv2D(32, (3, 3), activation='relu', padding='same'))\n", + "model.add(UpSampling2D((2, 2)))\n", + "model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))\n", + "model.add(UpSampling2D((2, 2)))\n", + "\n", + "model.add(Conv2D(3, (3, 3), activation='sigmoid', padding='same'))\n", + "\n", + "model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mse'])\n", + "model.summary()\n", + "\n", + "#Fit the model. \n", + "history = model.fit(\n", + " train_generator,\n", + " steps_per_epoch= 500 // batch_size,\n", + " epochs=1000,\n", + " validation_data=validation_generator,\n", + " validation_steps=75 // batch_size,\n", + " shuffle = True)\n", + "\n", + "\n", + "#plot the training and validation accuracy and loss at each epoch\n", + "loss = history.history['loss']\n", + "val_loss = history.history['val_loss']\n", + "epochs = range(1, len(loss) + 1)\n", + "plt.plot(epochs, loss, 'y', label='Training loss')\n", + "plt.plot(epochs, val_loss, 'r', label='Validation loss')\n", + "plt.title('Training and validation loss')\n", + "plt.xlabel('Epochs')\n", + "plt.ylabel('Loss')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get all batches generated by the datagen and pick a batch for prediction\n", + "#Just to test the model. \n", + "data_batch = [] #Capture all training batches as a numpy array\n", + "img_num = 0\n", + "while img_num <= train_generator.batch_index: #gets each generated batch of size batch_size\n", + " data = train_generator.next()\n", + " data_batch.append(data[0])\n", + " img_num = img_num + 1\n", + "\n", + "predicted = model.predict(data_batch[0]) #Predict on the first batch of images\n", + "\n", + "\n", + "#Sanity check, view few images and corresponding reconstructions\n", + "image_number = random.randint(0, predicted.shape[0])\n", + "plt.figure(figsize=(12, 6))\n", + "plt.subplot(121)\n", + "plt.imshow(data_batch[0][image_number])\n", + "plt.subplot(122)\n", + "plt.imshow(predicted[image_number])\n", + "plt.show()\n", + "\n", + "#Let us examine the reconstruction error between our validation data (good/normal images)\n", + "# and the anomaly images\n", + "validation_error = model.evaluate_generator(validation_generator)\n", + "anomaly_error = model.evaluate_generator(anomaly_generator)\n", + "\n", + "print(\"Recon. error for the validation (normal) data is: \", validation_error)\n", + "print(\"Recon. error for the anomaly data is: \", anomaly_error)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#Let us extract (or build) the encoder network, with trained weights.\n", + "#This is used to get the compressed output (latent space) of the input image. \n", + "#The compressed output is then used to calculate the KDE\n", + "\n", + "encoder_model = Sequential()\n", + "encoder_model.add(Conv2D(64, (3, 3), activation='relu', padding='same', input_shape=(SIZE, SIZE, 3), weights=model.layers[0].get_weights()) )\n", + "encoder_model.add(MaxPooling2D((2, 2), padding='same'))\n", + "encoder_model.add(Conv2D(32, (3, 3), activation='relu', padding='same', weights=model.layers[2].get_weights()))\n", + "encoder_model.add(MaxPooling2D((2, 2), padding='same'))\n", + "encoder_model.add(Conv2D(16, (3, 3), activation='relu', padding='same', weights=model.layers[4].get_weights()))\n", + "encoder_model.add(MaxPooling2D((2, 2), padding='same'))\n", + "encoder_model.summary()\n", + "\n", + "########################################################\n", + "# Calculate KDE using sklearn\n", + "from sklearn.neighbors import KernelDensity\n", + "\n", + "#Get encoded output of input images = Latent space\n", + "encoded_images = encoder_model.predict_generator(train_generator)\n", + "\n", + "# Flatten the encoder output because KDE from sklearn takes 1D vectors as input\n", + "encoder_output_shape = encoder_model.output_shape #Here, we have 16x16x16\n", + "out_vector_shape = encoder_output_shape[1]*encoder_output_shape[2]*encoder_output_shape[3]\n", + "\n", + "encoded_images_vector = [np.reshape(img, (out_vector_shape)) for img in encoded_images]\n", + "\n", + "#Fit KDE to the image latent data\n", + "kde = KernelDensity(kernel='gaussian', bandwidth=0.2).fit(encoded_images_vector)\n", + "\n", + "#Calculate density and reconstruction error to find their means values for\n", + "#good and anomaly images. \n", + "#We use these mean and sigma to set thresholds. \n", + "def calc_density_and_recon_error(batch_images):\n", + " \n", + " density_list=[]\n", + " recon_error_list=[]\n", + " for im in range(0, batch_images.shape[0]-1):\n", + " \n", + " img = batch_images[im]\n", + " img = img[np.newaxis, :,:,:]\n", + " encoded_img = encoder_model.predict([[img]]) # Create a compressed version of the image using the encoder\n", + " encoded_img = [np.reshape(img, (out_vector_shape)) for img in encoded_img] # Flatten the compressed image\n", + " density = kde.score_samples(encoded_img)[0] # get a density score for the new image\n", + " reconstruction = model.predict([[img]])\n", + " reconstruction_error = model.evaluate([reconstruction],[[img]], batch_size = 1)[0]\n", + " density_list.append(density)\n", + " recon_error_list.append(reconstruction_error)\n", + " \n", + " average_density = np.mean(np.array(density_list)) \n", + " stdev_density = np.std(np.array(density_list)) \n", + " \n", + " average_recon_error = np.mean(np.array(recon_error_list)) \n", + " stdev_recon_error = np.std(np.array(recon_error_list)) \n", + " \n", + " return average_density, stdev_density, average_recon_error, stdev_recon_error\n", + "\n", + "#Get average and std dev. of density and recon. error for uninfected and anomaly (parasited) images. \n", + "#For this let us generate a batch of images for each. \n", + "train_batch = train_generator.next()[0]\n", + "anomaly_batch = anomaly_generator.next()[0]\n", + "\n", + "uninfected_values = calc_density_and_recon_error(train_batch)\n", + "anomaly_values = calc_density_and_recon_error(anomaly_batch)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#Now, input unknown images and sort as Good or Anomaly\n", + "def check_anomaly(img_path):\n", + " density_threshold = 2500 #Set this value based on the above exercise\n", + " reconstruction_error_threshold = 0.004 # Set this value based on the above exercise\n", + " img = Image.open(img_path)\n", + " img = np.array(img.resize((128,128), Image.ANTIALIAS))\n", + " plt.imshow(img)\n", + " img = img / 255.\n", + " img = img[np.newaxis, :,:,:]\n", + " encoded_img = encoder_model.predict([[img]]) \n", + " encoded_img = [np.reshape(img, (out_vector_shape)) for img in encoded_img] \n", + " density = kde.score_samples(encoded_img)[0] \n", + "\n", + " reconstruction = model.predict([[img]])\n", + " reconstruction_error = model.evaluate([reconstruction],[[img]], batch_size = 1)[0]\n", + "\n", + " if density < density_threshold or reconstruction_error > reconstruction_error_threshold:\n", + " print(\"The image is an anomaly\")\n", + " \n", + " else:\n", + " print(\"The image is NOT an anomaly\")\n", + " \n", + " \n", + "#Load a couple of test images and verify whether they are reported as anomalies.\n", + "import glob\n", + "para_file_paths = glob.glob('cell_images2/parasitized/images/*')\n", + "uninfected_file_paths = glob.glob('cell_images2/uninfected_train/images/*')\n", + "\n", + "#Anomaly image verification\n", + "num=random.randint(0,len(para_file_paths)-1)\n", + "check_anomaly(para_file_paths[num])\n", + "\n", + "#Good/normal image verification\n", + "num=random.randint(0,len(para_file_paths)-1)\n", + "check_anomaly(uninfected_file_paths[num])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.4" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5b4d55c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,85 @@ +absl-py==1.4.0 +asttokens==2.2.1 +astunparse==1.6.3 +backcall==0.2.0 +cachetools==5.3.1 +certifi==2023.7.22 +charset-normalizer==3.2.0 +colorama==0.4.6 +comm==0.1.3 +contourpy==1.1.0 +cycler==0.11.0 +debugpy==1.6.7 +decorator==5.1.1 +executing==1.2.0 +flatbuffers==23.5.26 +fonttools==4.41.1 +gast==0.4.0 +google-auth==2.22.0 +google-auth-oauthlib==1.0.0 +google-pasta==0.2.0 +grpcio==1.56.2 +h5py==3.9.0 +idna==3.4 +imageio==2.31.1 +ipykernel==6.25.0 +ipython==8.14.0 +jedi==0.18.2 +jupyter_client==8.3.0 +jupyter_core==5.3.1 +keras==2.13.1 +kiwisolver==1.4.4 +lazy_loader==0.3 +libclang==16.0.6 +Markdown==3.4.4 +MarkupSafe==2.1.3 +matplotlib==3.7.2 +matplotlib-inline==0.1.6 +nest-asyncio==1.5.6 +networkx==3.1 +numpy==1.24.3 +oauthlib==3.2.2 +opencv-python==4.8.0.74 +opt-einsum==3.3.0 +packaging==23.1 +pandas==2.0.3 +parso==0.8.3 +pickleshare==0.7.5 +Pillow==10.0.0 +platformdirs==3.9.1 +prompt-toolkit==3.0.39 +protobuf==4.23.4 +psutil==5.9.5 +pure-eval==0.2.2 +pyasn1==0.5.0 +pyasn1-modules==0.3.0 +Pygments==2.15.1 +pyparsing==3.0.9 +python-dateutil==2.8.2 +pytz==2023.3 +PyWavelets==1.4.1 +pywin32==306 +pyzmq==25.1.0 +requests==2.31.0 +requests-oauthlib==1.3.1 +rsa==4.9 +scikit-image==0.21.0 +scipy==1.11.1 +six==1.16.0 +stack-data==0.6.2 +tensorboard==2.13.0 +tensorboard-data-server==0.7.1 +tensorflow==2.13.0 +tensorflow-estimator==2.13.0 +tensorflow-intel==2.13.0 +tensorflow-io-gcs-filesystem==0.31.0 +termcolor==2.3.0 +tifffile==2023.7.18 +tornado==6.3.2 +traitlets==5.9.0 +typing_extensions==4.5.0 +tzdata==2023.3 +urllib3==1.26.16 +wcwidth==0.2.6 +Werkzeug==2.3.6 +wrapt==1.15.0 -- GitLab