Measurement

On this page, we've described the assay we designed to measure maturation halftimes of chromoproteins.

Maturation assay

Introduction

Chromoproteins, such as GFP-like proteins, are extensively used as reporters in molecular biology due to their visible coloration and fluorescence. We look at them as potential candidates for biodyes. However, the maturation time of these proteins can vary significantly, affecting their effectiveness in industry. Accelerating the maturation process will hopefully contribute to a more sustainable use of dyes in textile industries.

The objective of this project was to generate faster-maturing variants of a slow-maturing chromoprotein through mutagenesis. By introducing specific mutations, we aimed to enhance the maturation without compromising the protein’s stability or color intensity. To analyze our mutations, we have been designing a maturation assay to evaluate the effects of these mutations and developing a computational pipeline to analyze the resulting data.

Maturation assay design

To assess the maturation kinetics of the mutated chromoproteins, we developed an assay that monitors the color development over time under controlled conditions.

Sample preparation

  • Bacterial Strains: Escherichia coli strains expressing either the green amilCP or mutated chromoprotein genes were used.
  • Growth Medium: LB agar plates supplemented with the appropriate antibiotics were prepared.
  • Anaerobic Growth: To prevent premature chromophore formation, plates were incubated anaerobically using a nitrogen gas atmosphere for 24 hours at 37°C.

Time-lapse imaging

After the anaerobic incubation, the plates were exposed to oxygen so it will initiate chromophore maturation. We then set up the camera so we could take picture of the plates progression. The plates were in room temp.

  • Camera Configuration: A high-resolution camera was mounted vertically above the samples, facing downward.
  • Background: A white sheet of paper was placed beneath the plates to ensure a consistent background for our image analysis.
  • Lighting Conditions: We tried to keep the lighting in the room as constant as possible. We had the room sealed off so no outside light could get in.
  • Time intervals: The camera was set to take a picture once every 5 minutes.
  • Duration: Two separate runs were conducted, lasting 48 hours and 96 hours, respectively.

Maturation assay analysis

The main goal for the maturation assay is to see how fast a the color of a certain chromoprotein develops. The analysis as a whole takes different points of the sample on the plate. We then measure the grayscale value of these coordinates for each picture/timestamp. We can then plot this data, and calculate the halftime of our samples.

Key functions

  • Timestamp Extraction: Calculate the timestamp of the photo
  • ROI Identification: Radius of interest, pick spots on sample
  • Process ROI: Extract the grayscale value of the ROI
  • Clean Data: Delete outliers in the data
  • Scatter plot: Visualize the change in grayscale value with a scatter plot and a curve that uses a smoothing function
  • Halftime Calculations: Calculate the halftime of the ROI
  • Boxplot: Create a boxplot for all the halftimes calculated for the sample and its ROIs

Functions explanations

ROI identification

The best way to know where a sample is located on a plate is to look at it yourself. The first script (get_coords.py) that you would run will have the last picture taken as the input. You will have this picture displayed with two input fields. The first one is input for the sample name. The second one is going to be for the color of the sample.

After defining your sample, you will be able to click on the picture where your sample is located. We took around 15 points per every sample to get a good overview of the samples progression. We recommend you take at least 10 points per sample if you have streaks on your plate as we have. If you have a smaller amount of colonies, choose all of these instead. Having more points for each sample will lead to a better analysis and clearer view of the halftime of the samples.


Image Analysis

The second script (image_analysis.py) will take the coordinates you have defined in the first script and extract the grayscale value of these coordinates for each picture. You will have to define the ROI size for this function. This will define the radius of the circle that will be used to extract the grayscale value. The measure of this is in pixels. We recommend you use a radius of 5 pixels. This will give you a good overview of the sample and will not be too big to overlap with other samples.

The grayscale value is a value between 0 and 255. 0 being black and 255 being white. The grayscale value of the sample will decrease over time as the chromophore matures. This is due to the fact that the chromophore will absorb more light as it matures. This will lead to less light being reflected back to the camera and thus give us a lower grayscale value. The grayscale value is calculated by using the cv2 library in python. This library is used for computer vision and image processing. It will take the grayscale value of the pixels in our ROIs and output the mean grayscale value of these pixels.

We will calculate the grayscale value for each of the ROIs for each picture. We will then save this data into a csv file. Here we will save the timestamp that is calculated from the picture name, the sample name, the color of the sample, the mean grayscale value of all of the ROI coordinates in the sample and a list of all the grayscale values for the ROIs in for the sample. This will give us a good overview of the data and will make it easier to analyze the data later on.


Scatter plots

The next step will be to create scatterplots of the data. This will give us a better understanding and a visual representation of the data. We will use a smoothing function to create somewhat of a trendline for our data. This will make it easier to see the trend of the data. We used the Savitzky–Golay filter as our smoothing function. This is a digital filter that can be applied to a set of data points. It will smooth the data and reduce noise. This will make it easier to see the trend of the data. We will plot the mean grayscale value of the combined ROIs for each sample. We will also plot the curve that represents the data. This will give us a good idea of how the grayscale value of the sample changes over time. This will also give us a clue to how fast the chromophore matures.


Boxplot

The last step will be to calculate the halftime and plot these in a boxplot. Here we will use the same principle as for the scatter plot. After cleaning the data of outliers, we will calculate the halftime of each ROI for each sample. We will do this by taking the first grayscale value of the ROI. We will take this value and subtract the lowest of the 10 last grayscale points of that ROI. We will divide this value by 2 and add this to the lowest grayscale value. This will give us the halftime grayscale value. We will then look for the time where this grayscale value is reached or for a value that is very close to this value. If we have that point, we also have the the time for this point, and thus have calculated the halftime of the sample. We will do this for each and every ROI of the sample and then plot these in a boxplot.

We create these boxplots because all grayscale values won't be the same due to where on the sample the ROI is located due to possible variations in cell density and lighting conditions. Creating a boxplot would not only show us a good overview of the data but also give us a good idea of the variation of the data. The boxplot would show us the mean value of the halftime as well as the standard deviation of the data and the outliers. This will give us a better overview of the halftimes. These calculations and plotting are done for all of the samples we have defined.

Troubleshooting

In the second run we had some troubles with the lighting. After about 14.5h, there was flickering in the light that was used to illuminate the samples. This led to a lot of noise and a shift in the data. This is likely due to the fact that we used the room's ceiling lights to illuminate the samples. This is not recommended as the lights can flicker and give you a lot of noise in the data. We recommend you use a constant light source that is not flickering like a battery driven lamp. This will give you a better result and a more accurate analysis of the data. We assume the ceiling lights flickered and then started illuminating brighter. This led to the shift in the graph and the grayscale value was higher than expected. To counter this, we looked at the difference in the grayscale values during the time the light flickered. We calculated the mean change in grayscale value during this period, and chose to reduce the grayscale value of all the points after the flickering with this value. This gave us a better result as this change resulted in a continuous curve. This is not a perfect solution, but it gave us a better result than if we would have left the data as it was. This is shown in Figure 1.

Illustration of the shift change
Figure 1: The figure shows the gap between both of the curves on the left graph and how the calculations of the mean change helped give us a better curve that is continuous.

Code:

get_coords.py


      import tkinter as tk
from tkinter import filedialog
import cv2
import csv
import os

# Initialize the main window
root = tk.Tk()
root.title("Bacteria Color Analysis")

# Variables to store user input
sample_name_var = tk.StringVar()
color_var = tk.StringVar()
image_path_var = tk.StringVar()
coordinates = []


# Function to open an image file
def select_image():
    file_path = filedialog.askopenfilename(
        title="Select Image", filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp")]
    )
    if file_path:
        image_path_var.set(file_path)
        print(f"Selected image: {file_path}")


def start_analysis():
    sample_name = sample_name_var.get()
    color = color_var.get()
    image_path = image_path_var.get()

    if not image_path:
        print("Please select an image.")
        return
    if not sample_name or not color:
        print("Please enter sample name and color.")
        return

    run_cv2_window(image_path, sample_name, color)


def run_cv2_window(image_path, sample_name, color):
    # Load the image
    image = cv2.imread(image_path)
    if image is None:
        print("Could not open image.")
        return

    coordinates.clear()  # Clear previous coordinates

    # Mouse callback function
    def click_event(event, x, y, flags, param):
        if event == cv2.EVENT_LBUTTONDOWN:
            # Store the coordinates in the list
            coordinates.append((x, y))
            print(f"Clicked at: ({x}, {y})")

            # Display the coordinates on the image window
            font = cv2.FONT_HERSHEY_SIMPLEX
            cv2.putText(image, f"({x},{y})", (x, y), font, 0.5, (255, 0, 0), 2)
            cv2.imshow(window_name, image)

            # Save the data to a CSV file
            file_exists = os.path.isfile("data.csv")
            with open("data.csv", "a", newline="") as csvfile:
                fieldnames = ["Sample Name", "Color", "X", "Y"]
                writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
                if not file_exists:
                    writer.writeheader()
                writer.writerow(
                    {"Sample Name": sample_name, "Color": color, "X": x, "Y": y}
                )

    window_name = "Image - Press q to quit"
    cv2.namedWindow(window_name)
    cv2.setMouseCallback(window_name, click_event)

    print("Click on the image to get coordinates. Press 'q' to quit.")

    def update_window():
        cv2.imshow(window_name, image)
        key = cv2.waitKey(20) & 0xFF
        if key == ord("q"):
            cv2.destroyAllWindows()
        else:
            root.after(20, update_window)

    update_window()


# GUI components for sample name and color input
control_frame = tk.Frame(root)
control_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)

sample_name_label = tk.Label(control_frame, text="Sample Name:")
sample_name_label.grid(row=0, column=0, padx=5, pady=5)
sample_name_entry = tk.Entry(control_frame, textvariable=sample_name_var)
sample_name_entry.grid(row=0, column=1, padx=5, pady=5)

color_label = tk.Label(control_frame, text="Color:")
color_label.grid(row=1, column=0, padx=5, pady=5)
color_entry = tk.Entry(control_frame, textvariable=color_var)
color_entry.grid(row=1, column=1, padx=5, pady=5)

# Buttons for selecting image and starting analysis
button_frame = tk.Frame(root)
button_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=10)

open_button = tk.Button(button_frame, text="Select Image", command=select_image)
open_button.pack(side=tk.LEFT, padx=5)

start_button = tk.Button(button_frame, text="Start Analysis", command=start_analysis)
start_button.pack(side=tk.LEFT, padx=5)

# Start the GUI event loop
root.mainloop()

    

image_analysis.py


      import os
      import subprocess
      import sys
      import cv2
      import numpy as np
      import csv
      from matplotlib import pyplot as plt
      import pandas as pd
      import re
      
      def extract_and_compute(filename, start_num):
          """
          Extract the last four digits from the filename and convert them to a timestamp value.
      
          :param filename: The name of the file.
          :return: Extracted timestamp value.
          """
          # Extract the last four digits from the filename using regular expression
          match = re.search(r'(\d{4})\.JPG$', filename)
          if not match:
              raise ValueError("Filename does not contain the required four digits pattern.")
      
          timestamp_value = int(match.group(1))
          computed_value = (timestamp_value * 5) - (start_num * 5)
          
          return computed_value
      
      def get_color_intensity(image, coordinates, roi_size=5):
          """
          Get the color intensity and vibrancy at the given coordinates.
          Coordinates should be a list of (x, y) tuples.
          """
          x, y = coordinates
          if 0 <= x < image.shape[1] and 0 <= y < image.shape[0]:
              x1, y1 = max(0, x - roi_size), max(0, y - roi_size)
              x2, y2 = min(image.shape[1], x + roi_size), min(image.shape[0], y + roi_size)
              
              roi = image[y1:y2, x1:x2]
              
              gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
              mean_gray_intensity = np.mean(gray_roi)
              
          else:
              mean_gray_intensity = -1
      
          return mean_gray_intensity
      
      
      def process_images(folder_path, coordinates, start_num, roi_size=5):
          """
          Process all images in the given folder and calculate color intensity and vibrancy
          at the specified coordinates.
          """
          df = pd.read_csv(coordinates)
          results = []
          for filename in os.listdir(folder_path):
              if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.bmp')):
                  image_path = os.path.join(folder_path, filename)
                  image = cv2.imread(image_path)
                  if image is not None:
                      computed_value = extract_and_compute(filename, start_num)
                      for sample in df['name'].unique().tolist():
                          grays = []
                          sample_coords = df[df['name'] == sample][['x', 'y']]
                          for _, row in sample_coords.iterrows():
                              coord = (row['x'], row['y'])
              # Compute the color intensity using your function
                              gray = get_color_intensity(image, coord, roi_size)
                              grays.append(gray)
      
                          results.append((computed_value, sample, df[df['name'] == sample]['color'].tolist()[0], np.mean(grays), grays))
          return results
      
      def csv_writer(results, path=''):
          headers = ['Time', 'Name', 'Color', 'Mean_gray', 'gray']
          csv_path = path
      
          with open(csv_path, mode='w', newline='') as csvfile:
              writer = csv.writer(csvfile)
              if csvfile.tell() == 0:
                  writer.writerow(headers)
              for result in results:
                  writer.writerow(result)
          
          df = pd.read_csv(csv_path)
          df_sorted = df.sort_values(by='Time')
          df_sorted.to_csv(csv_path, index=False)
    

graphs.py


      from matplotlib import pyplot as plt
      import numpy as np
      import pandas as pd
      from scipy.signal import savgol_filter
      import ast
      import seaborn as sns
      import os
      
      
      def halftime(x, y):
          """
          Calculate the halftime (the x-value where y reaches half of its total change).
          """
          # Ensure both x and y have the same length after dropping NaN
          if len(x) != len(y):
              min_length = min(len(x), len(y))
              x, y = x[:min_length], y[:min_length]
      
          # Calculate total change in y
          total_change = y.max() - y[-10:].min()
          halftime_y = y[-10:].min() + total_change / 2  # Y-value at halftime
      
          # Find the index of the y value closest to halftime_y
          halftime_index = np.abs(y - halftime_y).argmin()
          halftime_x = x[halftime_index]  # Get the corresponding x value
          # print(halftime_x/60)
          return halftime_x / 60
      
      
      def calculations(csv):
          """
          Process the CSV file and compute halftimes, applying necessary filters and transformations.
          """
          df = pd.read_csv(csv)
          data = []
      
          # Filter rows based on 'Mean_gray' values
          df = df[(df["Mean_gray"] >= 40) & (df["Mean_gray"] <= 160)]
      
          # Convert 'gray' from string to list if necessary
          df["gray"] = df["gray"].apply(
              lambda x: ast.literal_eval(x) if isinstance(x, str) else x
          )
      
          # Loop over each unique sample
          for sample in df["Name"].unique():
              sample_df = df[df["Name"] == sample]
              sample_df = sample_df.sort_values(by="Time")
              print(sample_df["Color"].values.tolist()[0])
      
              # Convert 'Time' and 'Mean_gray' to numeric types, handling errors
              x = (
                  pd.to_numeric(sample_df["Time"], errors="coerce").dropna().values
              )  # Convert to numpy array of floats
              y = (
                  pd.to_numeric(sample_df["Mean_gray"], errors="coerce").dropna().values
              )  # Convert to numpy array of floats
      
              halftimes_list = []
      
              # Check if 'gray' column is empty
              if not sample_df["gray"].iloc[0]:
                  print(f"No 'gray' data for sample {sample}. Skipping.")
                  continue
      
              # Determine the number of elements in 'gray' lists
              num_elements = len(sample_df["gray"].iloc[0])
      
              for i in range(num_elements):
                  gray = []
                  for j in sample_df["gray"].values.tolist():
                      # Ensure that each 'gray' entry is a list and has enough elements
                      if isinstance(j, list) and len(j) > i:
                          gray.append(j[i])
                      else:
                          gray.append(np.nan)  # Handle missing data
                  gray = np.array(gray)
      
                  # Compute halftime, handling NaN values
                  if np.isnan(gray).all():
                      halftimes_list.append(np.nan)
                  else:
                      # Remove NaN values from gray and corresponding x
                      valid_indices = ~np.isnan(gray) & ~np.isnan(x)
                      if np.any(valid_indices):
                          halftimes_list.append(
                              halftime(x[valid_indices], gray[valid_indices])
                          )
                      else:
                          halftimes_list.append(np.nan)
      
              # Apply Savitzky-Golay filter
              try:
                  yhat = savgol_filter(y, 52, 1)
              except Exception as e:
                  print(f"Error fitting Savitzky-Golay filter: {e}")
                  yhat = np.full_like(y, np.nan)  # Assign NaNs or handle as needed
      
              # Extract the first color from 'Color' column and strip quotes
              color_series = sample_df["Color"]
              if color_series.empty:
                  sample_color = "blue"  # Default color if 'Color' column is missing
                  print(
                      f"No 'Color' data for sample {sample}. Assigning default color 'blue'."
                  )
              else:
                  # Assuming all colors in the sample are the same
                  sample_color_raw = color_series.iloc[0]
                  if isinstance(sample_color_raw, str):
                      sample_color = sample_color_raw.strip(
                          "'\""
                      )  # Remove any surrounding quotes
                      print(
                          f"Assigned color '{sample_color}' to sample '{sample}'."
                      )  # Debugging
                  else:
                      sample_color = "blue"  # Default color if not a string
                      print(
                          f"Invalid 'Color' format for sample {sample}. Assigning default color 'blue'."
                      )
      
              data.append(
                  {
                      "sample": sample,
                      "x": list(x),
                      "y": list(y),
                      "yhat": list(yhat),
                      "color": sample_color,  # Corrected assignment
                      "halftimes": halftimes_list,
                  }
              )
      
          return pd.DataFrame(data)
      
      
      def scatter_plot(df, output):
          """
          Generate scatter plots for each sample, saving them as SVG files.
      
          Parameters:
          - df (pd.DataFrame): DataFrame containing plot data.
          - output (str): Directory to save the scatter plots.
          """
          if df is None or df.empty:
              print("DataFrame is empty or None, exiting scatter plot function.")
              return
      
          # Ensure the output directory exists
          os.makedirs(output, exist_ok=True)
      
          for _, row in df.iterrows():
              # Convert lists back to numpy arrays for plotting
              x = np.array(row["x"]) / 60  # Convert minutes to hours
              y = np.array(row["y"])
              yhat = np.array(row["yhat"])
              color = row["color"]
              print(color)
      
              # Validate and strip quotes from color
              if isinstance(color, str):
                  clean_color = color.strip("'\"")
              else:
                  clean_color = "black"  # Default color if not a string
                  print(
                      f"Invalid color format for sample {row['sample']}. Assigning default color 'blue'."
                  )
      
              print(
                  f"Plotting sample: {row['sample']} with color: {clean_color}"
              )  # Debugging
      
              try:
                  plt.scatter(x, y, marker="o", c=clean_color, label="Data Points")
                  if not np.isnan(yhat).all():
                      plt.plot(x, yhat, linestyle="--", color="black", label="Trendline")
              except ValueError as ve:
                  print(f"Error plotting sample {row['sample']}: {ve}")
                  continue  # Skip this plot if there's an error with color
      
              plt.xlabel("Time [h]")
              plt.ylabel("Grayscale value")
              plt.title(f'Grayscale scatterplot of {row["sample"]}')
              plt.grid(True)
      
              # Avoid duplicate legends
              handles, labels = plt.gca().get_legend_handles_labels()
              by_label = dict(zip(labels, handles))
              plt.legend(by_label.values(), by_label.keys())
      
              # Replace spaces with underscores for filenames
              sanitized_sample = row["sample"].replace(" ", "_")
              plot_filename = f"{output}/{sanitized_sample}.svg"
      
              # Save the plot
              plt.savefig(plot_filename)
              plt.close()
      
              print(f"Saved scatter plot to {plot_filename}")  # Debugging
      
      
      def prepare_boxplot_data(df):
          """
          Prepare data for boxplotting by aggregating halftime values for each sample.
      
          Parameters:
          - df (pd.DataFrame): DataFrame containing 'sample', 'halftimes', and 'color' columns.
      
          Returns:
          - samples (List[str]): List of sample names.
          - halftimes_data (List[List[float]]): List of halftime values per sample.
          - colors (List[str]): List of colors per sample.
          """
          samples = df["sample"].tolist()
          halftimes_data = df["halftimes"].tolist()
          colors = df["color"].tolist()
      
          # Ensure that each 'halftimes' entry is a list
          halftimes_data = [ht if isinstance(ht, list) else [] for ht in halftimes_data]
      
          # Optionally, remove NaN values
          halftimes_data = [
              [ht for ht in sample_ht if not np.isnan(ht) and ht < 48]
              for sample_ht in halftimes_data
          ]
      
          return samples, halftimes_data, colors
      
      
      def boxplot_with_points_seaborn(df, output):
          """
          Create a boxplot for each sample with halftime points overlayed using Seaborn.
      
          Parameters:
          - df (pd.DataFrame): DataFrame containing 'sample', 'halftimes', and 'color' columns.
          - output (str): Directory to save the boxplot image.
          """
      
          # Prepare data
          samples, halftimes_data, colors = prepare_boxplot_data(df)
      
          # Create a palette dict mapping samples to their colors
          palette = dict(zip(samples, colors))
      
          # Remove samples with no halftime data
          samples_filtered = []
          halftimes_filtered = []
          for sample, halftimes in zip(samples, halftimes_data):
              if halftimes:  # Only include samples with at least one halftime value
                  samples_filtered.append(sample)
                  halftimes_filtered.append(halftimes)
      
          if not halftimes_filtered:
              print("No halftime data available for boxplot.")
              return
      
          # Create a long-form DataFrame suitable for Seaborn
          boxplot_df = pd.DataFrame(
              {
                  "sample": np.repeat(
                      samples_filtered, [len(ht) for ht in halftimes_filtered]
                  ),
                  "halftime": np.concatenate(halftimes_filtered),
              }
          )
      
          # Remove NaN halftime values (if any)
          boxplot_df = boxplot_df.dropna(subset=["halftime"])
      
          # Create the boxplot with Seaborn
          plt.figure(figsize=(12, 8))
          sns.boxplot(
              x="sample", y="halftime", data=boxplot_df, palette=palette, showfliers=False
          )
          sns.stripplot(
              x="sample",
              y="halftime",
              data=boxplot_df,
              color="black",
              size=3,
              jitter=True,
              alpha=0.6,
          )
      
          # Customize plot
          plt.xlabel("Sample")
          plt.ylabel("Halftime Values [h]")
          plt.title("Halftime Distribution per Sample")
          plt.xticks(rotation=45, ha="right")  # Rotate sample names for better readability
          plt.grid(True, axis="y", linestyle="--", alpha=0.7)
      
          # Adjust layout to prevent clipping of tick-labels
          plt.tight_layout()
      
          # Ensure the output directory exists
          os.makedirs(output, exist_ok=True)
      
          # Save the plot
          plt.savefig(f"{output}/figname.svg")
          plt.close()
      
          print(f"Seaborn boxplot saved to {output}/figname.svg")
      
      
      # Execute the calculations function and generate scatter plots
      data_df = calculations("final_wiki/analysis/new_output_th.csv")
      scatter_plot(data_df)
      
      # Generate the boxplot with halftime points
      boxplot_with_points_seaborn(data_df)
    

fix_gap.py


      import pandas as pd
      import numpy as np
      import ast
      
      
      def fix_gap(csv_path, t_start=865, t_stop=980, t_onward=980, t_end=5755):
          df = pd.read_csv(csv_path)
      
          for sample in df["Name"].unique():
              sample_df = df[df["Name"] == sample]
              grayscale_shift_db = sample_df[
                  (sample_df["Time"] >= t_start - 5) & (sample_df["Time"] <= t_stop)
              ]["Mean_gray"].tolist()
      
              large_gray = []
              small_gray = [grayscale_shift_db[0]]
      
              for i in range(len(grayscale_shift_db[1:])):
                  if grayscale_shift_db[i] > small_gray[0] + 1:
                      large_gray.append(grayscale_shift_db[i])
                  else:
                      small_gray.append(grayscale_shift_db[i])
      
              change = np.mean(large_gray) - np.mean(small_gray)
      
              for i in range(t_start, t_end, 5):
      
                  gray_str = df.loc[(df["Name"] == sample) & (df["Time"] == i), "gray"].values
                  gray_list = ast.literal_eval(gray_str[0])
      
                  df.loc[(df["Name"] == sample) & (df["Time"] == i), "Mean_gray"] = (
                      df.loc[(df["Name"] == sample) & (df["Time"] == i), "Mean_gray"].values
                      - change
                  )
      
                  adjusted_gray_list = [x - change for x in gray_list]
      
                  df.loc[(df["Name"] == sample) & (df["Time"] == i), "gray"] = str(
                      adjusted_gray_list
                  )
      
          df = df.sort_values(by="Time")
          df.to_csv("output_th.csv")
      
      
      fix_gap("input.csv")