Tempeh Tech

Fermentation Heat


By Noël Jung


Content

  1. Intro
  2. Imports
  3. Code
  4. Conclusion

Intro

In the following, I will go through the results of my very first attempt to capture the tempeh fermentation visually. I had a camera and an LED light installed inside my incubator. A picture was taken every 20 minutes. I also monitored the temperature inside the tempeh. I installed a second thermometer at another spot in the incubator to follow its temperature development. Let's see how it looks! Spoiler: Not everything went smoothly.

Imports

In [11]:
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.

In [12]:
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.

In [13]:
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.

In [14]:
# 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")

Code

Our ferm_data dataframe comprises of one row per measurement. Each row contains the temperature data that the two sensors measured as well as the file name of the corresponding image. We can read the names of the respective jpeg files in the rows of the dataframe and load them as numpy arrays. Since the camera was positioned upside down in the incubator, we need to rotate the images. We then write them to an output mp4 file. Each frame is shown for half a second, one second of video corresponds to 40 minutes real time, as the pictures were taken every 20 minutes.
In [15]:
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.

In [20]:
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.

  1. The video is shaky. The camera was taped to one side of the incubator, but the tape was not very strong. So the camera gradually slipped down. I tried to stick it on again, but it didn't help much. I will use stronger tape for the next run.
  2. The tempeh is not completely in focus. I use the first Raspberry Pi camera module which has a fixed focus, so the only way to move something into the focus is by moving it further from or closer to the camera. The tempeh is as far away as possible from the camera, on the opposite side of the incubator, but it is not enough. I don't think it’s too bad though. So, I will continue with this camera for now and see if I can get my hands on the third camera module at some point.
  3. It's a bit dark. I can adjust that by controlling aperture of the camera and brightness of the LED. This should be an easy fix.
But enough criticism. What can we learn? Well, the fungus grows around the chickpeas like we want it to. First some gray structures are forming around the peas that slowly become lighter forming dense white mycelium.

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.


Take a close look at the lower right corner of the tempeh between 0:26 and 0:37. The mycelium (the white tissue around the chickpeas), seems to be contracting. Could this be an artefact of the pictures? Or is it the fungal actually expanding and retracting?
Let's see how the temperature developed.
In [17]:
# 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.

In [18]:
# 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).

In [19]:
# 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));

Conclusion

The temperature sensors worked as expected. I think the quality of the video is just good enough, but I need to toy around with the brightness and fixate the device better. To get a better resolution in future experiments, I would need a better camera. I am unsure what produces the fluctuating spots in the tempeh.
It is evident that the tempeh produces heat as the temperature measured inside exceeds the incubator temperature after some time. This is the metabolic heat of the tempeh culture, consisting of a fungus, some bacteria and possibly some yeasts! While they grow and proliferate, their cells perform biochemical reactions that produce heat as a side product. I am thrilled that this can actually be measured!