{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### GAN Art Creating Kaggle Project","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport time\n\nimport tensorflow as tf\nfrom tensorflow.keras import layers\nimport shutil","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:07.575219Z","iopub.execute_input":"2025-03-15T20:13:07.575694Z","iopub.status.idle":"2025-03-15T20:13:20.574381Z","shell.execute_reply.started":"2025-03-15T20:13:07.575631Z","shell.execute_reply":"2025-03-15T20:13:20.573754Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Brief Description of the Problem and Data","metadata":{}},{"cell_type":"markdown","source":"The task in this competition is to create 7,000 to 10,000 pictures that Monet-style pictures. That is, the task is to create pictures that look like they could have been pained by Claude Monet and that a trained classifier cannot distinguish as fakes.\n\nThe competition comes with a dataset that consists of 300 Monet paintings (sized 256x256 in JPEG format) and 7028 photos (sized 256x256 in JPEG format). The aim is to use the Monet painting to train a GAN (Generative Adversarial Network) to master the style of Monet and use the GAN to style the pictures in a way that they look like they could have been pained by Monet. Alternatively, the GAN can simply generate images in the style of Monet without making use of the pictures, which is the approach we choose for this project.\n\nThe dataset also contains copies of the paintings and pictures in the TFRecords format. The TFRecords are supposed to be faster to process for tenosorflow and for that reason we will make use of them in our project.  ","metadata":{}},{"cell_type":"markdown","source":"### Exploratory Data Analysis (EDA) — Inspect, Visualize and Clean the Data","metadata":{}},{"cell_type":"markdown","source":"In this section, we will ...\n* verify that we can access all avialable files\n* control that there are 300 paintings and 7028 photos\n* visually inspect a few paintings and photos\n* Comapare the color intensity of paintings and photos","metadata":{}},{"cell_type":"markdown","source":"### Utility functions","metadata":{}},{"cell_type":"code","source":"def count_images_in_folder(folder_path):\n    \"\"\"\n    Counts the number of image files in a given directory.\n    \n    Args:\n    folder_path (str): The path to the directory containing images.\n    \n    Returns:\n    int: The number of image files in the directory.\n    \"\"\"\n    # List of image file extensions you want to check for\n    image_extensions = ('.jpg', ) # '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'\n    \n    # Get a list of files in the directory\n    files_in_folder = os.listdir(folder_path)\n    \n    # Count files that end with any of the image extensions\n    image_count = sum(1 for file in files_in_folder if file.lower().endswith(image_extensions))\n    \n    return image_count","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:20.575463Z","iopub.execute_input":"2025-03-15T20:13:20.575960Z","iopub.status.idle":"2025-03-15T20:13:20.580300Z","shell.execute_reply.started":"2025-03-15T20:13:20.575925Z","shell.execute_reply":"2025-03-15T20:13:20.579378Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def display_images(path):\n    # List all JPEG files in the specified directory\n    files = [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.jpg')]\n    \n    # Select the first 9 images (or fewer if not enough images are available)\n    files = files[:9]\n    \n    # Set up the figure and axes for a 3x3 grid\n    fig, axes = plt.subplots(3, 3, figsize=(10, 10))\n    axes = axes.ravel()\n    \n    # Loop over the files and the axes\n    for ax, img_path in zip(axes, files):\n        # Open and display the image\n        img = Image.open(img_path)\n        ax.imshow(img)\n        ax.set_xticks([])\n        ax.set_yticks([])\n        ax.set_title(os.path.basename(img_path))\n    \n    # Hide any unused axes if there are less than 9 images\n    for ax in axes[len(files):]:\n        ax.axis('off')\n    \n    plt.tight_layout()\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:20.582338Z","iopub.execute_input":"2025-03-15T20:13:20.582646Z","iopub.status.idle":"2025-03-15T20:13:20.613000Z","shell.execute_reply.started":"2025-03-15T20:13:20.582615Z","shell.execute_reply":"2025-03-15T20:13:20.612364Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compare_image_colors_normalized(path1, path2):\n    \"\"\"\n    Compares the normalized average RGB values between two sets of images from different folders,\n    using distinct color shades for better visual distinction.\n\n    Args:\n    path1 (str): Path to the first directory containing pictures.\n    path2 (str): Path to the second directory containing paintings.\n    \"\"\"\n    def get_avg_colors(image_folder):\n        image_files = [os.path.join(image_folder, f) for f in os.listdir(image_folder) if f.endswith(('png', 'jpg', 'jpeg'))]\n        avg_colors = []\n        for file in image_files:\n            with Image.open(file) as img:\n                img = img.convert('RGB')\n                data = np.array(img)\n                mean_color = data.mean(axis=(0, 1))\n                avg_colors.append(mean_color)\n        return np.array(avg_colors)\n    \n    # Get average colors for both directories\n    avg_colors1 = get_avg_colors(path1)  # Pictures\n    avg_colors2 = get_avg_colors(path2)  # Paintings\n    \n    # Plotting\n    fig, axes = plt.subplots(3, 1, figsize=(8, 10), sharex=True)\n    color_labels = ['Red', 'Green', 'Blue']\n    dark_shades = ['darkred', 'darkgreen', 'darkblue']  # Darker shades for pictures\n    pastel_shades = ['salmon', 'lightgreen', 'lightblue']  # Pastel shades for paintings\n    \n    for i in range(3):\n        axes[i].hist(avg_colors1[:, i], bins=30, alpha=0.7, label='Pictures', color=dark_shades[i], density=True)\n        axes[i].hist(avg_colors2[:, i], bins=30, alpha=0.7, label='Paintings', color=pastel_shades[i], density=True, linestyle='dashed')\n        axes[i].set_title(f'Normalized Comparison of {color_labels[i]} Channel')\n        axes[i].legend(loc='upper right')\n\n    plt.tight_layout()\n    plt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:20.614070Z","iopub.execute_input":"2025-03-15T20:13:20.614267Z","iopub.status.idle":"2025-03-15T20:13:20.627228Z","shell.execute_reply.started":"2025-03-15T20:13:20.614246Z","shell.execute_reply":"2025-03-15T20:13:20.626439Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Problem Investigation","metadata":{}},{"cell_type":"code","source":"# Overview of available files\nInput = '/kaggle/input/gan-getting-started'\nprint(os.listdir(Input))","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:20.628026Z","iopub.execute_input":"2025-03-15T20:13:20.628295Z","iopub.status.idle":"2025-03-15T20:13:20.645868Z","shell.execute_reply.started":"2025-03-15T20:13:20.628268Z","shell.execute_reply":"2025-03-15T20:13:20.645103Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Count number of pictures\ncount_images_in_folder(Input + '/photo_jpg')","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:20.646675Z","iopub.execute_input":"2025-03-15T20:13:20.646909Z","iopub.status.idle":"2025-03-15T20:13:20.834594Z","shell.execute_reply.started":"2025-03-15T20:13:20.646891Z","shell.execute_reply":"2025-03-15T20:13:20.833896Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Count number of paintings\ncount_images_in_folder(Input + '/monet_jpg')","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:20.835389Z","iopub.execute_input":"2025-03-15T20:13:20.835619Z","iopub.status.idle":"2025-03-15T20:13:20.858401Z","shell.execute_reply.started":"2025-03-15T20:13:20.835598Z","shell.execute_reply":"2025-03-15T20:13:20.857824Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Photo\ndisplay_images(Input + '/photo_jpg')","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:20.859032Z","iopub.execute_input":"2025-03-15T20:13:20.859221Z","iopub.status.idle":"2025-03-15T20:13:21.824842Z","shell.execute_reply.started":"2025-03-15T20:13:20.859205Z","shell.execute_reply":"2025-03-15T20:13:21.823779Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Monet Paintings\ndisplay_images(Input + '/monet_jpg')","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:21.827363Z","iopub.execute_input":"2025-03-15T20:13:21.827615Z","iopub.status.idle":"2025-03-15T20:13:22.809101Z","shell.execute_reply.started":"2025-03-15T20:13:21.827595Z","shell.execute_reply":"2025-03-15T20:13:22.807986Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"compare_image_colors_normalized(Input + '/monet_jpg', Input + '/photo_jpg')","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:13:22.810597Z","iopub.execute_input":"2025-03-15T20:13:22.811001Z","iopub.status.idle":"2025-03-15T20:14:35.340717Z","shell.execute_reply.started":"2025-03-15T20:13:22.810964Z","shell.execute_reply":"2025-03-15T20:14:35.339833Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Conclusion","metadata":{"execution":{"iopub.execute_input":"2024-05-03T05:19:28.965266Z","iopub.status.busy":"2024-05-03T05:19:28.964863Z","iopub.status.idle":"2024-05-03T05:19:29.003520Z","shell.execute_reply":"2024-05-03T05:19:29.000950Z","shell.execute_reply.started":"2024-05-03T05:19:28.965235Z"}}},{"cell_type":"markdown","source":"Based on our investigations, we can conclude that:\n\n* The dataset contains paintings and pictures in the formats jpg and TFrecords\n* There are 300 paintings in the dataset as adverticed in the competition, but there are 7038 pictures, which are 10 more that adverticed.\n* Visual inspection of images verfied that both the pictures and the paintings looked correct. \n* An analysis of the color intensity found that the pictures in average made use of more intense colors that the paintings. The paintings are a bit muted compared to the pictures\n\nAll in all, this implies that there are good reasons to believe that the dataset can be used to train a functioning GAN.","metadata":{}},{"cell_type":"markdown","source":"### Model Architecture","metadata":{}},{"cell_type":"markdown","source":"This project makes use of a Deep Convolutional Generative Adversarial Network (DCGAN) architecture. The architecture consists of two main pars:\n\nThe **Generator**, which takes a random noise vector as input and outputs a synthetic image. The generator tries to produce images that are indistinguishable from real images, effectively \"fooling\" the discriminator. To accomplish this task, it utilizes a series of transposed convolutional layers to progressively upscale the input vector into a full-sized image.\n\nThe **Discriminator** acts as a critic that tries to differentiate between real images (from the dataset) and fake images (produced by the generator). It uses standard convolutional layers to downsample the input image and makes a decision on its authenticity.\n\nDuring training the generator tries to maximize the error rate of the discriminator (by improving the quality of the fake images), while the discriminator tries to minimize its own error rate. This adversarial process drives both networks to improve continuously, with the generator producing increasingly Monet-like painings and the discriminator becoming better at detecting subtleties between real and fake paintings. The training alternates between updating the discriminator with real and fake images, and updating the generator based on the feedback from the discriminator.","metadata":{}},{"cell_type":"markdown","source":"### Data handeling functions","metadata":{"execution":{"iopub.execute_input":"2024-05-03T05:19:28.965266Z","iopub.status.busy":"2024-05-03T05:19:28.964863Z","iopub.status.idle":"2024-05-03T05:19:29.003520Z","shell.execute_reply":"2024-05-03T05:19:29.000950Z","shell.execute_reply.started":"2024-05-03T05:19:28.965235Z"}}},{"cell_type":"code","source":"# Define the function to parse TFRecords\ndef _parse_function(proto):\n    features = {\n        'image': tf.io.FixedLenFeature([], tf.string),\n    }\n    parsed_features = tf.io.parse_single_example(proto, features)\n    image = tf.io.decode_jpeg(parsed_features['image'], channels=3)\n    image = tf.cast(image, tf.float32)\n    image = (image - 127.5) / 127.5\n    image = tf.reshape(image, [256, 256, 3])\n    return image\n\n# Load the dataset from TFRecords\ndef load_dataset(tfrecord_files, batch_size):\n    dataset = tf.data.TFRecordDataset(tfrecord_files)\n    dataset = dataset.map(_parse_function, num_parallel_calls=tf.data.AUTOTUNE)\n    dataset = dataset.shuffle(buffer_size=10000)\n    dataset = dataset.batch(batch_size)\n    dataset = dataset.prefetch(buffer_size=tf.data.AUTOTUNE)\n    return dataset","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:14:35.341560Z","iopub.execute_input":"2025-03-15T20:14:35.341792Z","iopub.status.idle":"2025-03-15T20:14:35.347098Z","shell.execute_reply.started":"2025-03-15T20:14:35.341772Z","shell.execute_reply":"2025-03-15T20:14:35.346245Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### GAN model","metadata":{"execution":{"iopub.execute_input":"2024-05-03T05:19:28.965266Z","iopub.status.busy":"2024-05-03T05:19:28.964863Z","iopub.status.idle":"2024-05-03T05:19:29.003520Z","shell.execute_reply":"2024-05-03T05:19:29.000950Z","shell.execute_reply.started":"2024-05-03T05:19:28.965235Z"}}},{"cell_type":"code","source":"# Generator model\ndef make_generator_model():\n    model = tf.keras.Sequential([\n        layers.Dense(8*8*1024, use_bias=False, input_shape=(100,)),\n        layers.BatchNormalization(),\n        layers.LeakyReLU(),\n        layers.Reshape((8, 8, 1024)),\n        layers.Conv2DTranspose(512, (5, 5), strides=(2, 2), padding='same', use_bias=False),\n        layers.BatchNormalization(),\n        layers.LeakyReLU(),\n        layers.Conv2DTranspose(256, (5, 5), strides=(2, 2), padding='same', use_bias=False),\n        layers.BatchNormalization(),\n        layers.LeakyReLU(),\n        layers.Conv2DTranspose(128, (5, 5), strides=(2, 2), padding='same', use_bias=False),\n        layers.BatchNormalization(),\n        layers.LeakyReLU(),\n        layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False),\n        layers.BatchNormalization(),\n        layers.LeakyReLU(),\n        layers.Conv2DTranspose(3, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')\n    ])\n    return model\n\n# Discriminator model\ndef make_discriminator_model():\n    model = tf.keras.Sequential([\n        layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[256, 256, 3]),\n        layers.LeakyReLU(),\n        layers.Dropout(0.3),\n        layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'),\n        layers.LeakyReLU(),\n        layers.Dropout(0.3),\n        layers.Conv2D(256, (5, 5), strides=(2, 2), padding='same'),\n        layers.LeakyReLU(),\n        layers.Dropout(0.3),\n        layers.Conv2D(512, (5, 5), strides=(2, 2), padding='same'),\n        layers.LeakyReLU(),\n        layers.Dropout(0.3),\n        layers.Flatten(),\n        layers.Dense(1)\n    ])\n    return model\n\n# Define loss functions\ncross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)\n\ndef generator_loss(fake_output):\n    return cross_entropy(tf.ones_like(fake_output), fake_output)\n\ndef discriminator_loss(real_output, fake_output):\n    real_loss = cross_entropy(tf.ones_like(real_output), real_output)\n    fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)\n    total_loss = real_loss + fake_loss\n    return total_loss\n\n@tf.function\ndef train_step(images, generator, discriminator, generator_optimizer, discriminator_optimizer):\n    noise = tf.random.normal([tf.shape(images)[0], 100])  # Use dynamic sizing for batch size\n\n    with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:\n        generated_images = generator(noise, training=True)\n        real_output = discriminator(images, training=True)\n        fake_output = discriminator(generated_images, training=True)\n\n        gen_loss = generator_loss(fake_output)\n        disc_loss = discriminator_loss(real_output, fake_output)\n\n    gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)\n    gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)\n    generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))\n    discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))\n    return gen_loss, disc_loss\n\ndef train(dataset, epochs, generator, discriminator, generator_optimizer, discriminator_optimizer):\n    for epoch in range(epochs):\n        start = time.time()\n        \n        gen_loss_list = []\n        disc_loss_list = []\n\n        for image_batch in dataset:\n            gen_loss, disc_loss = train_step(image_batch, generator, discriminator, generator_optimizer, discriminator_optimizer)\n            gen_loss_list.append(gen_loss)\n            disc_loss_list.append(disc_loss)\n\n        gen_loss_avg = tf.reduce_mean(gen_loss_list)\n        disc_loss_avg = tf.reduce_mean(disc_loss_list)\n        \n        #print(f'Epoch {epoch+1}, gen_loss={gen_loss_avg:.4f}, disc_loss={disc_loss_avg:.4f}, time={time.time()-start:.2f} sec')\n\n        if (epoch + 1) == 1 or (epoch + 1) % 20 == 0:\n            print(f'Epoch {epoch+1}, gen_loss={gen_loss_avg:.4f}, disc_loss={disc_loss_avg:.4f}, time={time.time()-start:.2f} sec')\n            generate_and_save_images(generator, epoch + 1, seed)\n\ndef generate_and_save_images(model, epoch, test_input):\n    predictions = model(test_input, training=False)\n    fig = plt.figure(figsize=(8, 8))\n\n    for i in range(predictions.shape[0]):\n        plt.subplot(4, 4, i + 1)\n        img = (predictions[i, :, :, :] * 127.5 + 127.5).numpy().astype('uint8')\n        plt.imshow(img)\n        plt.axis('off')\n\n    plt.show()\n\nBATCH_SIZE = 32\ntfrecord_files = [\n    Input + '/monet_tfrec/monet00-60.tfrec',\n    Input + '/monet_tfrec/monet04-60.tfrec',\n    Input + '/monet_tfrec/monet08-60.tfrec',\n    Input + '/monet_tfrec/monet12-60.tfrec',\n    Input + '/monet_tfrec/monet16-60.tfrec'\n]\ndataset = load_dataset(tfrecord_files, BATCH_SIZE)\n\ngenerator = make_generator_model()\ndiscriminator = make_discriminator_model()\n\ngenerator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\ndiscriminator_optimizer = tf.keras.optimizers.Adam(2e-4, beta_1=0.5)\n\nnum_examples_to_generate = 4\nseed = tf.random.normal([num_examples_to_generate, 100])\n\n# Start Training\ntrain(dataset, 1500, generator, discriminator, generator_optimizer, discriminator_optimizer)\n","metadata":{"execution":{"iopub.status.busy":"2025-03-15T20:14:35.347909Z","iopub.execute_input":"2025-03-15T20:14:35.348095Z","iopub.status.idle":"2025-03-15T21:44:00.285380Z","shell.execute_reply.started":"2025-03-15T20:14:35.348078Z","shell.execute_reply":"2025-03-15T21:44:00.284618Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_and_save_images(model, total_images, directory, batch_size=10):\n    if not os.path.exists(directory):\n        os.makedirs(directory)  # Ensure the directory exists\n\n    for batch_start in range(0, total_images, batch_size):\n        batch_end = min(batch_start + batch_size, total_images)\n        num_images = batch_end - batch_start\n        noise = tf.random.normal([num_images, 100])  # Generate different noises for each batch\n        predictions = model(noise, training=False)\n\n        for i, img in enumerate(predictions):\n            img = (img * 127.5 + 127.5).numpy().astype('uint8')\n            image = Image.fromarray(img)\n            image.save(f\"{directory}/image_{batch_start+i+1:03d}.jpg\", 'JPEG')  # Save each image as JPEG\n\n            # Directory where images will be saved\noutput_dir = '../images'\n\n# After training, generate and save 100 images in JPEG format\ngenerate_and_save_images(generator, 7000, output_dir)","metadata":{"execution":{"iopub.status.busy":"2025-03-15T21:44:00.286395Z","iopub.execute_input":"2025-03-15T21:44:00.286742Z","iopub.status.idle":"2025-03-15T21:44:25.950742Z","shell.execute_reply.started":"2025-03-15T21:44:00.286711Z","shell.execute_reply":"2025-03-15T21:44:25.949851Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display some generated images\npath = '../images'\ndisplay_images(path)","metadata":{"execution":{"iopub.status.busy":"2025-03-15T21:44:25.951508Z","iopub.execute_input":"2025-03-15T21:44:25.951770Z","iopub.status.idle":"2025-03-15T21:44:26.902075Z","shell.execute_reply.started":"2025-03-15T21:44:25.951750Z","shell.execute_reply":"2025-03-15T21:44:26.901046Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Conclusion","metadata":{"execution":{"iopub.execute_input":"2024-05-03T05:19:28.965266Z","iopub.status.busy":"2024-05-03T05:19:28.964863Z","iopub.status.idle":"2024-05-03T05:19:29.003520Z","shell.execute_reply":"2024-05-03T05:19:29.000950Z","shell.execute_reply.started":"2024-05-03T05:19:28.965235Z"}}},{"cell_type":"markdown","source":"In this assignment, we have seen that it is possible to build an GAN model that can produce Monet-style paintings. However, with the training time used in this project, it not difficult for a human to distinguish a real from a fake Monet. The GAN gets the colors right, but the figurative aspects of the paintings are wrong or missing. More training time could perhaps have improved the figurative aspects of the paintings. But it is far from certain that the GAN would have been able to learn these aspects of the Monet-style. \n\nWe chose to create a DCGAN that created the images from scratch (so to speak) rather than training a cycle GAN that could style pictures. It would have been interesting to compare the two approaches, but that falls outside the scope of this mini-project.  ","metadata":{}},{"cell_type":"code","source":"import zipfile\n\nsdir = \"../images/\"\nzfile = \"images.zip\"\nwith zipfile.ZipFile(\"images.zip\",\"w\") as zipf:\n    for file in os.listdir(\"../images/\"):\n        fpath = os.path.join(sdir,file)\n        zipf.write(fpath, os.path.basename(file))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-15T22:10:59.869545Z","iopub.execute_input":"2025-03-15T22:10:59.869894Z","iopub.status.idle":"2025-03-15T22:11:00.744404Z","shell.execute_reply.started":"2025-03-15T22:10:59.869867Z","shell.execute_reply":"2025-03-15T22:11:00.743370Z"}},"outputs":[],"execution_count":null}]}