ccpem / ccpem/mrcfile

Extended header in SERI format

Open
#71 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
86
Forks
24
PR merge metrics
No merged PRs in 30d

Description

Hi Colin,

As discussed last week, here is my (Copilot supported) code for dealing with SERI extended headers. If you give me some pointers on where to start, I might give it a try to fit it into your code myself.

Thanks again and best,
Fabian

```python
def readExtendedHeader(self):
"""Read extended header."""

with open(self.file_path, "rb") as mrc_file:
mrc = mrc_file.read(4096)

# MRC header format SERI (SERI is not covered by mrcfile)
format = ""
for char in struct.iter_unpack("s", mrc[104:108]):
format += char[0].decode("utf-8")

if format == "SERI":
# Get number of sections
section_number = struct.unpack("i", mrc[8: 12])[0]

# Get bytes per section
bytes_per_section = struct.unpack("h", mrc[128: 130])[0]

# Bitflags
self.extended_header_bitflags = struct.unpack("h", mrc[130: 132])[0]
"""
https://bio3d.colorado.edu/imod/doc/mrc_format.txt
1 = Tilt angle in degrees * 100 (2 bytes)
2 = X, Y, Z piece coordinates for montage (6 bytes)
4 = X, Y stage position in microns * 25 (4 bytes)
8 = Magnification / 100 (2 bytes)
16 = Intensity * 25000 (2 bytes)
32 = Exposure dose in e-/A2, a float in 4 bytes
128, 512: Reserved for 4-byte items
64, 256, 1024: Reserved for 2-byte items
If the number of bytes implied by these flags does
not add up to the value in nint, then nint and nreal
are interpreted as ints and reals per section
"""

section_data = []
for i in range(1024, 1024 + bytes_per_section * section_number, bytes_per_section): # extended header starts at byte 1024
section_data.append(self.parseSectionData(mrc[i:i + bytes_per_section], self.extended_header_bitflags))

self.extended_header = section_data

@staticmethod
def parseSectionData(section_data, bitflags):
"""Parse a single section of extended header data based on bitflags."""

parsed_data = {}
offset = 0

# Check each bitflag and extract corresponding data
if bitflags & 1: # Tilt angle in degrees * 100 (2 bytes)
parsed_data["TiltAngle"] = struct.unpack('h', section_data[offset:offset + 2])[0] / 100
offset += 2

if bitflags & 2: # X, Y, Z piece coordinates for montage (6 bytes)
parsed_data["PieceCoordinates"] = struct.unpack('hhh', section_data[offset:offset + 6])
offset += 6

if bitflags & 4: # X, Y stage position in microns * 25 (4 bytes)
parsed_data["StagePosition"] = tuple(coord / 25 for coord in struct.unpack('hh', section_data[offset:offset + 4]))
offset += 4

if bitflags & 8: # Magnification / 100 (2 bytes)
parsed_data["Magnification"] = struct.unpack('h', section_data[offset:offset + 2])[0] * 100
offset += 2

if bitflags & 16: # Intensity * 25000 (2 bytes)
parsed_data["Intensity"] = struct.unpack('h', section_data[offset:offset + 2])[0] / 25000
offset += 2

if bitflags & 32: # Exposure dose in e-/A² (4 bytes, float)
parsed_data["ExposureDose"] = struct.unpack('f', section_data[offset:offset + 4])[0]
offset += 4

# Reserved fields (optional, if needed)
if bitflags & 128 or bitflags & 512: # Reserved for 4-byte items
offset += 4
if bitflags & 64 or bitflags & 256 or bitflags & 1024: # Reserved for 2-byte items
offset += 2

return parsed_data

def writeExtendedHeader(self, file_path=None):
"""Write extended header."""

file_path = Path(file_path) if file_path else self.file_path

# If extended header comes from SERI format, it is a list
if isinstance(self.extended_header, (list, tuple)):
with open(file_path, "r+b") as mrc_file:
# Write the extended header type (e.g., "SERI") to bytes 104–108
mrc_file.seek(104)
mrc_file.write(b"SERI")

# Write the number of sections to bytes 8–12
section_number = len(self.extended_header)
mrc_file.seek(8)
mrc_file.write(struct.pack("i", section_number))

# Write the bytes per section to bytes 128–130
bytes_per_section = len(self.serializeSectionData(self.extended_header[0], self.extended_header_bitflags))
mrc_file.seek(128)
mrc_file.write(struct.pack("h", bytes_per_section))

# Write the bitflags to bytes 130–132
mrc_file.seek(130)
mrc_file.write(struct.pack("h", self.extended_header_bitflags))

# Write the section data
mrc_file.seek(1024)
mrc_file.write(b"".join([self.serializeSectionData(section, self.extended_header_bitflags) for section in self.extended_header]))

# If extended header comes from FEI format, use mrcfile built-in function
else:
with mrcfile.open(file_path, "r+") as mrc_file:
mrc_file.set_extended_header(self.extended_header)

@staticmethod
def serializeSectionData(section_data, bitflags):
"""Convert parsed section data back to bytes based on bitflags."""

section_bytes = b""

# Serialize each field based on the bitflags
if bitflags & 1: # Tilt angle in degrees * 100 (2 bytes)
tilt_angle = int(section_data.get("TiltAngle", 0) * 100)
section_bytes += struct.pack('h', tilt_angle)

if bitflags & 2: # X, Y, Z piece coordinates for montage (6 bytes)
piece_coordinates = section_data.get("PieceCoordinates", (0, 0, 0))
section_bytes += struct.pack('hhh', *piece_coordinates)

if bitflags & 4: # X, Y stage position in microns * 25 (4 bytes)
stage_position = tuple(int(coord * 25) for coord in section_data.get("StagePosition", (0, 0)))
section_bytes += struct.pack('hh', *stage_position)

if bitflags & 8: # Magnification / 100 (2 bytes)
magnification = int(section_data.get("Magnification", 0) / 100)
section_bytes += struct.pack('h', magnification)

if bitflags & 16: # Intensity * 25000 (2 bytes)
intensity = int(section_data.get("Intensity", 0) * 25000)
section_bytes += struct.pack('h', intensity)

if bitflags & 32: # Exposure dose in e-/A² (4 bytes, float)
exposure_dose = section_data.get("ExposureDose", 0.0)
section_bytes += struct.pack('f', exposure_dose)

# Reserved fields (optional, if needed)
if bitflags & 128 or bitflags & 512: # Reserved for 4-byte items
section_bytes += b'\x00\x00\x00\x00'
if bitflags & 64 or bitflags & 256 or bitflags & 1024: # Reserved for 2-byte items
section_bytes += b'\x00\x00'

return section_bytes
```

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by locating the existing extended-header entry points and compare them with the proposed readExtendedHeader, writeExtendedHeader, parseSectionData, and serializeSectionData methods in the issue. Check how mrcfile currently handles FEI headers and the SERI format specification. Done means SERI extended headers can be read and written without regressing existing extended-header behavior, with coverage for the supported bitflags.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
data
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.