By Noël Jung
import os
import cv2
import seaborn as sns
import pandas as pd
from IPython.display import display, HTML
from matplotlib import animation
import matplotlib.pyplot as plt
import graph_style # Contains styling parameters for the plots.
from project_utils import time_parser, add_time_columns
As before, we load the data and add some additional columns stating how much fermentation time has passed.
IMAGE_DIR = r"../runV2"
CSV_FILE = r"../CSV/runV2.csv"
EX_ID = "V2"
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 = add_time_columns(ferm_data, new_columns=["h_passed", "min_passed"])
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'].
Let's take a look at the data.
ferm_data.head()
| Time | name_dev_1 | T_dev_1 | name_dev_2 | T_dev_2 | T_CPU | im | led | FSR_value | FSR_voltage | min_passed | h_passed | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 16072023-09:33:48 | 28-3c01f09505ec | 25.750 | 28-3c01f0953253 | 25.437 | 51.6 | 16072023-093348.jpeg | ok | 0 | 0.0 | 0 | 0 |
| 1 | 16072023-09:44:22 | 28-3c01f09505ec | 41.812 | 28-3c01f0953253 | 25.937 | 48.2 | 16072023-094422.jpeg | ok | 0 | 0.0 | 10 | 0 |
| 2 | 16072023-09:54:55 | 28-3c01f09505ec | 34.750 | 28-3c01f0953253 | 25.937 | 52.1 | 16072023-095455.jpeg | ok | 0 | 0.0 | 21 | 0 |
| 3 | 16072023-10:05:29 | 28-3c01f09505ec | 30.750 | 28-3c01f0953253 | 28.250 | 49.6 | 16072023-100529.jpeg | ok | 0 | 0.0 | 31 | 1 |
| 4 | 16072023-10:16:03 | 28-3c01f09505ec | 29.062 | 28-3c01f0953253 | 28.312 | 51.1 | 16072023-101603.jpeg | ok | 0 | 0.0 | 42 | 1 |
As before, we first produce the video. This time, I took an image every 10 minutes.
if display_only:
video_filename = f"./assets/{EX_ID}-zoom-medium.mp4"
else:
video_filename = f"../OutputVideos/{EX_ID}-zoom-medium.mp4"
# Functions for collecting and zooming in on each image.
def get_image_from_path(basename, dir_path=IMAGE_DIR, rotate=True, img_transformers=[]):
"""Load a single image, potentially turn, and return it."""
path = os.path.join(dir_path, basename)
im = cv2.imread(path)
if rotate:
im = cv2.rotate(im, cv2.ROTATE_180)
for transformer, args in img_transformers:
im = transformer(im, *args)
return im
def zoom_in(im, x_1, x_2, y_1, y_2):
"""Zooms in on a picture."""
return im[y_1:y_2, x_1:x_2]
# We assemble the video, while we zoom in a little bit.
if not display_only:
im_files_iter = (get_image_from_path(row, img_transformers=[(zoom_in, (300, 1100, 30, 350))]) for row in ferm_data["im"])
# Exactly this encoding needs to be used.
video_writer = cv2.VideoWriter(video_filename, 0x00000020 , 15, (800, 320))
for im_file_name in im_files_iter:
video_writer.write(im_file_name)
# Display the video.
video_html = HTML(f'<video controls src="{video_filename}" width="640" height="360"></video>')
display(video_html)
Ok, looks good. The video is much less shaky, so one can really see what's going on. We can clearly see the fluctuations we observed before. These are most noticeable in areas where the mycelium has not yet grown a lot, so especially in the beginning. Our hypothesis was, that these somehow have to do with the temperature fluctuations in the incubator. We can check this by viewing the video and the incubator temperature side by side.
if display_only:
ani_filename = f"./assets/{EX_ID}-cond-vs-temp.mp4"
else:
ani_filename = f"../OutputVideos/{EX_ID}-cond-vs-temp.mp4"
# Set theme for graphs.
axes_style = sns.axes_style(style=graph_style.style)
sns.set(rc={"figure.figsize":(10, 4), 'animation.embed_limit': 2**128})
sns.set_theme(style=axes_style, palette=graph_style.colors_pomegranate ,font=graph_style.FONT)
# Collect the images.
im_files_iter = (get_image_from_path(row, img_transformers=[(zoom_in, (300, 1100, 30, 350))]) for row in ferm_data["im"])
im_list = list(im_files_iter)
# Produce an "empty" figure with two subplots.
fig, ax = plt.subplots(2,1, figsize=(10, 10), gridspec_kw={'height_ratios': [1, 1]})
fig.tight_layout(pad=5)
fig.suptitle("Incubator temperature and condensation", **graph_style.super_title_style)
# Update empty figure frame by frame.
def animate(i):
"""Find ith temperature value and corresponding image."""
# Put image in lower subplot.
ax[1] = plt.imshow(cv2.cvtColor(im_list[i], cv2.COLOR_RGB2BGR))
plt.axis('off')
# Set plot axis parameters.
ax[0].set_ylim(25,42)
ax[0].set_xlim(0,1000)
# Plot and style temperature curve.
df_index = i + 1
x_range = ferm_data["min_passed"].iloc[:df_index]
y_range = ferm_data["T_dev_1"].iloc[:df_index]
ani_T_plot = sns.lineplot(ax=ax[0], x=x_range, y=y_range, color=graph_style.colors_pomegranate[1])
ani_T_plot.set_xlabel(xlabel="Time in minutes", **graph_style.axes_style)
ani_T_plot.set_ylabel(ylabel="Temperature in °C", **graph_style.axes_style)
ani_T_plot.tick_params( **graph_style.tick_style)
# We assemble the animation.
if not display_only:
ani = animation.FuncAnimation(fig, animate, frames=94, interval=300) # frames=94
ani_save = animation.FFMpegWriter(fps=5)
ani.save(ani_filename, writer=ani_save)
# Display the animation video.
video_html = HTML(f'<video controls src="{ani_filename}" width="1280" height="720"></video>')
display(video_html)
Success! There is indeed a correlation between the temperature spikes and the fluctuating spots on the tempeh. As the incubator temperature decreases, bright looking condensation drops appear on the inside of the plastic bag. When the temperature suddenly rises, these drops disappear at once. This cycle repeats until the mycelium is too dense to allow for any drops to appear.
Take another look at the video. You see two sensors stuck into the tempeh. The one coming from the right is the thermometer. The one on the left is new: It is the FSR. These are essentially buttons that can sense how hard they are being pressed. The theory was that as growth advances, the tempeh becomes harder, and the mycelium increases its pressure on the FSR. The signal would hence be a proxy measure for the mycelium strength. Let's see whether that worked!
# Initialize graph.
FSR_plot = sns.lineplot(data=ferm_data, x="min_passed", y="FSR_value", **graph_style.lineplot_kwargs)
# Set labels and apply styles.
FSR_plot.set_title(label="FSR curve", **graph_style.title_style)
FSR_plot.set_xlabel(xlabel="Time in minutes", **graph_style.axes_style)
FSR_plot.set_ylabel(ylabel="Signal", **graph_style.axes_style)
FSR_plot.tick_params( **graph_style.tick_style)
FSR_plot.set(xlim=(0,4150), ylim=(0, 4));
It is relatively hard to take the tempeh out of its plastic bag. It seemed to me like it had increased in size and would therefore not slide out easily. I thought this increase in size is facilitated by the mycelium pushing the chickpeas away from one another, which I would be able to monitor with the FSR: The peas would push against the "button".
The moment I held the tempeh in my hand, I realized my previous theory was wrong. It was easy to remove the sensor, it was not grown in or otherwise stuck. Hence there was no pressure against the FSR. I now believe that the mycelium does the opposite: Instead of pushing the chickpeas apart, it pulls them together. But since it can't grow through, this does not work between chickpeas of opposite sides of the sensors. The fact that it is hard to remove it from the plastic bag has most likely to do with the tempeh block becoming more rigid over the fermentation time.
