Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
4 changes: 2 additions & 2 deletions docs/photosynthesis_read_cropreporter.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ PSII_data instance containing [xarray DataArrays](http://xarray.pydata.org/en/st
- Measurements from chlorophyll fluorescence are stored in the attribute `chlorophyll` and include a chlorophyll fluorescence frame (Chl) stored as NumPy array/ndarray. The Fdark frame, if collected, is not stored.
- Green fluorescence protein (GFP) measurements are stored in the attribute `gfp` and include frames for dark fluorescence (Fdark), GFP fluorescence (GFP, 525 nm), and autofluorescence (Auto, 585 nm).
- Red fluorescence protein (RFP) measurements are stored in the attribute `rfp` and include frames for dark fluorescence (Fdark) and RFP fluorescence (585 nm).
- Alpha light absorption coefficient (APH) measurements are stored in the attribute `aph` and include reflected light frames for red (640 nm) and far-red (732 nm) wavelengths.
- Alpha light absorption coefficient (APH) measurements are stored in the attribute `aph` and include reflected light frames for red (640 nm) and far-red (732 nm) wavelengths. `ps.aph` is a NumPy array/ndarray with shape (height, width, 2), where index 0 is the red frame and index 1 is the far-red frame. The Fdark frame, if collected, is not stored.
- Spectral measurements are stored as a PlantCV [Spectral_data](Spectral_data.md) object in the attribute
`spectral`. Frames are stored by reflectance wavelength and can include: blue (475nm), green (550nm), red (640nm),
green2 (540nm), far-red (710nm), and near-infrared (770nm).
Expand Down Expand Up @@ -81,6 +81,6 @@ ps.ojip_dark.plot(col='frame_label', col_wrap=4)

**Alpha light absorption coefficient (APH) measurements**

![Screenshot](img/documentation_images/photosynthesis_read_cropreporter/6_aph_frames.png)
![Screenshot](img/documentation_images/photosynthesis_read_cropreporter/6_APH-frames.png)
Comment thread
kmurphy61 marked this conversation as resolved.

**Source Code:** [Here](https://github.com/danforthcenter/plantcv/blob/main/plantcv/plantcv/photosynthesis/read_cropreporter.py)
53 changes: 29 additions & 24 deletions plantcv/plantcv/photosynthesis/read_cropreporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,42 +579,47 @@ def _process_rfp_data(ps, metadata):


def _process_aph_data(ps, metadata):
"""
Create an xarray DataArray for an APH dataset.
"""Read APH dataset and keep the Red and FarRed frames as a NumPy array.

Parameters
----------
ps : plantcv.plantcv.classes.PSII_data
PSII_data instance.
metadata : dict
INF file metadata dictionary.

"""
bin_filepath = _dat_filepath(dataset="APH", datapath=ps.datapath, filename=ps.filename)

if os.path.exists(bin_filepath):
img_cube, frame_labels, frame_nums = _read_dat_file(dataset="APH", filename=bin_filepath,
height=int(metadata["ImageRows"]),
width=int(metadata["ImageCols"]))
frame_labels = ["Red", "FarRed"]
aph = xr.DataArray(
data=img_cube,
dims=('x', 'y', 'frame_label'),
coords={'frame_label': frame_labels,
'frame_num': ('frame_label', frame_nums)},
name='aph'
)
aph.attrs["long_name"] = "Alpha Light absorption coefficient (Reflection) (640nm Red, 732nm FarRed)"
aph.attrs["dark_comp_on"] = int(metadata.get("AphDarkCompOn", metadata.get("AlphaDarkCompOn", "0")))
aph.attrs["gain_red"] = float(metadata.get("AphGainRed", metadata.get("AlphaGainRed", "nan")))
aph.attrs["gain_farred"] = float(metadata.get("AphGainFarRed", metadata.get("AlphaGainFarRed", "nan")))
ps.add_data(aph)
# Read the raw data cube (contains Fdark, Red, and FarRed or just Red and FarRed)
img_cube, _, _ = _read_dat_file(dataset="APH", filename=bin_filepath,
height=int(metadata["ImageRows"]),
width=int(metadata["ImageCols"]))

_debug(
visual=ps.aph,
filename=os.path.join(params.debug_outdir, f"{str(params.device)}_APH-frames.png"),
col='frame_label',
col_wrap=int(np.ceil(ps.aph.frame_label.size / 4))
# The APH file typically has: index 0 = Fdark, index 1 = Red, index 2 = FarRed.
# Some acquisitions may only contain two frames (e.g. no dark frame).
# Select the Red and FarRed frames based on the number of frames present.
# Use the last 2 frames as Red and FarRed:
# - When there are 3 frames, indices are [0]=Fdark, [1]=Red, [2]=FarRed -> use indices 1 and 2.
# - When there are 2 frames, indices are [0]=Red, [1]=FarRed -> use indices 0 and 1.
num_frames = img_cube.shape[2]
if num_frames < 2:
raise RuntimeError(f"APH DAT file contains {num_frames} frame(s); expected at least 2 (Red and FarRed).")
aph_frames = img_cube[:, :, num_frames - 2:num_frames]

# Store as a standard attribute
ps.aph = aph_frames

# Debugging — wrap in a temporary DataArray so frame labels appear in the plot
aph_debug = xr.DataArray(
data=ps.aph,
dims=('x', 'y', 'frame_label'),
coords={'frame_label': ['Red', 'FarRed']}
)
Comment thread
kmurphy61 marked this conversation as resolved.
_debug(visual=aph_debug,
filename=os.path.join(params.debug_outdir, f"{str(params.device)}_APH-frames.png"),
col='frame_label',
col_wrap=2)


def _dat_filepath(dataset, datapath, filename):
Expand Down
22 changes: 22 additions & 0 deletions tests/plantcv/photosynthesis/test_read_cropreporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,28 @@ def test_read_cropreporter_aph_only(photosynthesis_test_data, tmpdir):
assert ps.aph.shape[2] == 2 # Red + FarRed


def test_read_cropreporter_aph_insufficient_frames(photosynthesis_test_data, tmpdir, monkeypatch):
"""Test that APH import raises RuntimeError when DAT file contains fewer than 2 frames."""
cache_dir = tmpdir.mkdir("sub_aph_err")
inf_dest = os.path.join(cache_dir, "HDR_2025-12-12_tob1_20251212205712029.INF")
dat_dest = os.path.join(cache_dir, "APH_2025-12-12_tob1_20251212205712029.DAT")
shutil.copyfile(photosynthesis_test_data.cropreporter_aph, inf_dest)
aph_dat = photosynthesis_test_data.cropreporter_aph.replace("HDR", "APH").replace("INF", "DAT")
shutil.copyfile(aph_dat, dat_dest)
# Override image dimensions so monkeypatched data is the right size
with open(inf_dest, "a") as f:
f.write("\nImageRows=10")
f.write("\nImageCols=10")
# Return only 1 frame worth of data (10 * 10 * 1 = 100) to trigger the error
monkeypatch.setattr(np, "fromfile", lambda *args, **kwargs: np.ones(100, dtype=np.uint16))
error_raised = False
try:
read_cropreporter(filename=inf_dest)
except RuntimeError:
error_raised = True
assert error_raised


def test_read_cropreporter_pmt_only_9_labels(photosynthesis_test_data, tmpdir):
"""Test PMT (PAM Time) import with 9 frames."""
# Create a test tmp directory
Expand Down