By Noël Jung
import os
import cv2
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from IPython.display import display, HTML
from ipywidgets import Video
import graph_style # Contains styling parameters for the plots.
from project_utils import time_parser, time_dif
First, we load the data. Since the fermentation went on over several days, let's make sure to include a column stating how many hours have passed since the beginning of the experiment.
IMAGE_DIR = r"../runV1"
CSV_FILE = r"../CSV/runV1.csv"
display_only = True
ferm_data = pd.read_csv(CSV_FILE, parse_dates=["Time"], date_parser=time_parser)
# Transform time column into a readable format.
ferm_data["Time"] = ferm_data["Time"].dt.strftime("%d%m%Y-%H:%M:%S")
ferm_data["min_passed"] = ferm_data.apply(lambda row: time_dif(ferm_data.iloc[0]["Time"],
row["Time"],input_format="%d%m%Y-%H:%M:%S" ,resolution="min"), axis=1)
ferm_data["h_passed"] = ferm_data.apply(lambda row: time_dif(ferm_data.iloc[0]["Time"],
row["Time"], input_format="%d%m%Y-%H:%M:%S" ,resolution="h"), axis=1)
print(f'Values sensor 1: {ferm_data["name_dev_1"].unique()}. Values sensor 2: {ferm_data["name_dev_2"].unique()}.')
Values sensor 1: ['28-3c01f09505ec']. Values sensor 2: ['28-3c01f0953253'].
One measurement with one of the thermometers failed.
display(ferm_data.loc[ferm_data["T_dev_2"] == "failure"])
| Time | name_dev_1 | T_dev_1 | name_dev_2 | T_dev_2 | T_CPU | im | led | min_passed | h_passed | |
|---|---|---|---|---|---|---|---|---|---|---|
| 128 | 07052023-05:36:37 | 28-3c01f09505ec | 36.937 | 28-3c01f0953253 | failure | 50.1 | 07052023-053637.jpeg | ok | 2631 | 44 |
We need to fill in another value to avoid python throwing exceptions further down. We could use the average value over all measurements that this thermometer measured in the experiment. But I rather use the forward fill strategy, meaning that the value in the row above is replacing the missing value. In this case, this is the measurement just before.
# All values that cannot be transformed into a float are replaced with NaN.
ferm_data['T_dev_2'] = pd.to_numeric(ferm_data['T_dev_2'], errors='coerce').astype('Float64')
# NaN values are replaced via forward fill.
ferm_data['T_dev_2'] = ferm_data['T_dev_2'].fillna(method="ffill")
def get_image_from_path(basename, dir_path=IMAGE_DIR, rotate=True):
"""Load a single image, potentially turn, and return it."""
path = os.path.join(dir_path, basename)
im = cv2.imread(path)
if rotate:
return cv2.rotate(im, cv2.ROTATE_180)
return im
if not display_only:
im_files_iter = (get_image_from_path(row) for row in ferm_data["im"])
# The exact encoding muss be used.
video_writer = cv2.VideoWriter("../OutputVideos/full_video_slow_rotate180.mp4", 0x00000020 , 2, (1280, 720))
for im_file_name in im_files_iter:
video_writer.write(im_file_name)
Great. Now let's take a look.
video_HTML = HTML(f'<video controls src="./assets/full_video_slow_rotate180.mp4" width="640" height="360"></video>')
display(video_HTML)
Yikes. This feels like sitting in a broken AR roller coaster. Let's look at the issues one by one and see what we can learn.
This process is quicker on the top than on the bottom. I saw this also in previous unrecorded fermentations. First, I thought that could be an issue of oxygen availability in the contact area between the incubator and the tempeh. But looking at it now from this angle, it is clear that the tempeh grows slower on bottom areas that don't touch the incubator. A lack of oxygen cannot be the explanation. I believe that either the fungus generally prefers growing upwards over growing downwards, or the temperature is too high in the bottom to allow for optimal growth.
# Set theme for graphs.
axes_style = sns.axes_style(style=graph_style.style)
sns.set(rc={"figure.figsize":(10, 4), }) # Warning! Overrides themes, set rc in style dict above.
sns.set_theme( style=axes_style, palette=graph_style.colors_pomegranate ,font=graph_style.FONT)
# Rearrange dataframe from wide to long form (seaborn requirement).
long_T_dev_df = ferm_data.melt(id_vars=["min_passed"],
value_vars=["T_dev_2", "T_dev_1"], var_name="dev_nr", value_name="T_in_C" )
# Initialize graphs
ferm_plot = sns.lineplot(data=long_T_dev_df, y="T_in_C", x="min_passed", hue="dev_nr", **graph_style.lineplot_kwargs)
# Apply legend styles.
handles, labels = ferm_plot.get_legend_handles_labels()
legend_title = ferm_plot.legend(title="Temperature", handles=handles,
labels=["Incubator", "Tempeh"], loc="lower right",**graph_style.legend_style)
plt.setp(legend_title.get_title(), **graph_style.legend_title_style)
# Apply graph styles.
ferm_plot.set_title(label="Heat production inside Tempeh", **graph_style.title_style)
ferm_plot.set_xlabel(xlabel="Time in minutes", **graph_style.axes_style)
ferm_plot.set_ylabel(ylabel="Temperature in °C", **graph_style.axes_style)
ferm_plot.tick_params( **graph_style.tick_style)
ferm_plot.set(xlim=(0,3500), ylim=(20, 40));
The incubator temperature fluctuates a lot. I had a hunch that my 10 € yogurt incubator might not be the best the market had to offer. But the magnitude of the swings surprises me. The incubator was set to 30 °C. The thermometer was placed right at the bottom of the incubator, where the heat comes from. Let's see the average and maximum temperature measured.
# The incubator is not turned on during the first and last measurements.
data_points_turned_on = ferm_data.loc[1:159]
max_inc_T = data_points_turned_on["T_dev_2"].max()
mean_inc_T = data_points_turned_on["T_dev_2"].mean()
std_inc_T = data_points_turned_on["T_dev_2"].std()
print(f"Highest incubator temperature: {max_inc_T:.2f} °C. Average incubator temperature:{mean_inc_T:.2f}±{std_inc_T:.2f} °C.")
Highest incubator temperature: 38.31 °C. Average incubator temperature:32.57±2.96 °C.
Ok, the average temperature on the incubator bottom is about 2.5 °C higher than what I had set it to. The fluctuations are likely not ideal for the fungal growth. But then again, the fungus did grow, so I believe it is fine. Before we continue, let's redo the plot with an hours time scale as this is easier to imagine. As a nice side effect, the curve will be smoothed. This is because the plotted hour values are each the average of three values (t-20 min, t, t+20 min).
# Rearrange dataframe from wide to long form (seaborn requirement).
long_T_dev_df = ferm_data.melt(id_vars=["h_passed"],
value_vars=["T_dev_2", "T_dev_1"], var_name="dev_nr", value_name="T_in_C")
# Initialize graphs.
ferm_plot = sns.lineplot(data=long_T_dev_df, y="T_in_C", x="h_passed",
hue="dev_nr", errorbar=("ci", 0), **graph_style.lineplot_kwargs)
# Apply legend styles.
handles, labels = ferm_plot.get_legend_handles_labels()
legend_title = ferm_plot.legend(title="Temperature", handles=handles,
labels=["Incubator", "Tempeh",], loc="lower right", **graph_style.legend_style)
plt.setp(legend_title.get_title(), **graph_style.legend_title_style)
# Set labels and apply styles.
ferm_plot.set_title(label="Heat production inside Tempeh", **graph_style.title_style)
ferm_plot.set_xlabel(xlabel="Time in hours", **graph_style.axes_style)
ferm_plot.set_ylabel(ylabel="Temperature in °C", **graph_style.axes_style)
ferm_plot.tick_params( **graph_style.tick_style)
ferm_plot.set(xlim=(0,58), ylim=(20, 40));