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
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
[submodule "lib/param"]
path = lib/param
url = https://github.com/spaceinventor/libparam/
branch = 59d3eb12d562582e37140e470afb71ce9bb18f8f
branch = 768970c6320a455250ddd88903bbd9f58db81216
112 changes: 112 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

**Build the project:**
```bash
./build.sh
```
This removes the build directory, runs meson setup, and builds with ninja.

**Run the application:**
```bash
./run.sh
```
Runs with default parameters: ZMQ interface, node 163, localhost device.

**Manual execution:**
```bash
./build/Disco2CameraControl -i INTERFACE -d DEVICE -n NODE -p PORT
```
- INTERFACE: zmq, can, or kiss (default: zmq)
- DEVICE: connection device (default: localhost)
- NODE: CSP node address (default: 2)
- PORT: server listening port (default: 10)

## Architecture Overview

### Core Components

**CaptureController** (`src/camera_control/capture_controller.cpp`)
- Main orchestrator that handles capture requests
- Creates appropriate camera controller instances based on camera type
- Manages the entire capture workflow from parameter validation to image delivery

**Camera Controllers** (Abstract factory pattern)
- `VimbaController`: Handles Allied Vision cameras via VimbaX SDK
- `IRController`: Handles infrared cameras
- `TestController`: Provides mock camera functionality for testing
- All inherit from `CameraController` base class

**MessageQueue** (`src/message_queue/message_queue.cpp`)
- Manages System V shared memory and message queues
- Handles image data transfer to the image processing pipeline (DIPP)
- Creates shared memory segments and sends notification messages

**CSP Server** (`src/communication/csp_server.c`)
- Provides CSP (Cubesat Space Protocol) communication interface
- Exposes parameters via libparam for remote control
- Handles different transport interfaces (ZMQ, CAN, KISS)

### Data Flow

1. **Parameter Setting**: Ground station sets capture parameters via CSP
2. **Capture Trigger**: `capture_param` set to 1 triggers image acquisition
3. **Controller Selection**: CaptureController creates appropriate camera controller
4. **Image Capture**: Camera controller captures raw BayerRG images (12-bit)
5. **Data Packaging**: Images packaged with metadata into batch structure
6. **Memory Management**: Image data stored in System V shared memory
7. **Pipeline Notification**: Message queue notifies DIPP of ready images

### Key Data Structures

**ImageBatch**: System V message queue structure containing image metadata and shared memory key
**Image**: Individual image structure with dimensions, pixel data, and timestamps
**CaptureMessage**: Internal capture instruction format with all parameters

### Build System

- **Meson** build system with cross-compilation support
- Automatically detects ARM64 vs x86_64 and links appropriate VimbaX libraries
- Dependencies: CSP, libparam, OpenCV, Protobuf, VimbaX SDK

### Camera Parameters (CSP exposed)

- `camera_id_param`: Camera model string (e.g., "1800 U-500c")
- `camera_type_param`: 0=VMB, 1=IR, 2=TEST
- `exposure_param`: Exposure in microseconds
- `iso_param`: Gain/ISO value
- `num_images_param`: Number of images in burst
- `interval_param`: Delay between images (microseconds)
- `obid_param`: Unique batch identifier
- `pipeline_id_param`: Target processing pipeline ID
- `capture_param`: Set to 1 to trigger capture
- `error_log`: Latest error code (read-only)

### Error Handling

Error codes are defined in `include/utils/errors.hpp`:
- 100s: Parameter parsing errors
- 200s: Camera/capture errors
- 300s: Message queue/shared memory errors

## Development Notes

### Prerequisites
```bash
sudo apt-get install libzmq3-dev libbsd
```

**Protobuf 3.19** is required - clone from the 3.19.x branch and build manually.

### Testing
- Use `camera_type_param = 2` (TEST) for testing without hardware
- Test executable `receiver` in build directory can receive image messages
- Debug mode available with `-D` flag

### VimbaX Integration
- Cross-compilation support for ARM64 vs Linux x86_64
- VimbaX SDK libraries automatically selected based on build target
- Raw BayerRG format captured at 12-bit depth for maximum processing flexibility
88 changes: 88 additions & 0 deletions analyze_raw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
import numpy as np
from pathlib import Path

def analyze_bayer_raw():
"""Analyze the raw Bayer buffer data."""
# Image parameters (full resolution)
width = 2464
height = 2056

with open('buffer_dump.bin', 'rb') as f:
data = f.read()

# Convert to 16-bit values (little-endian)
raw_data = np.frombuffer(data, dtype=np.uint16)
# Mask to 12-bit values
raw_data = raw_data & 0x0FFF

# Reshape to image dimensions
image = raw_data.reshape(height, width)

print(f"Raw Bayer Data Analysis:")
print(f"Shape: {image.shape}")
print(f"Data type: {image.dtype}")
print(f"Min value: {image.min()}")
print(f"Max value: {image.max()}")
print(f"Mean value: {image.mean():.2f}")
print(f"Std deviation: {image.std():.2f}")

# Check for saturation
saturated_pixels = np.sum(image >= 4095) # 12-bit max
total_pixels = width * height
saturation_percent = (saturated_pixels / total_pixels) * 100
print(f"Saturated pixels (4095): {saturated_pixels} ({saturation_percent:.2f}%)")

# Check for zero pixels
zero_pixels = np.sum(image == 0)
zero_percent = (zero_pixels / total_pixels) * 100
print(f"Zero pixels: {zero_pixels} ({zero_percent:.2f}%)")

# Sample different regions
print(f"\nSample regions:")
regions = [
("Top-left", image[0:100, 0:100]),
("Top-right", image[0:100, -100:]),
("Center", image[height//2-50:height//2+50, width//2-50:width//2+50]),
("Bottom-left", image[-100:, 0:100]),
("Bottom-right", image[-100:, -100:])
]

for name, region in regions:
print(f" {name}: min={region.min()}, max={region.max()}, mean={region.mean():.1f}")

# Check histogram
hist, bins = np.histogram(image, bins=50, range=(0, 4095))
print(f"\nHistogram analysis:")
print(f" Most common value: {bins[np.argmax(hist)]:.0f}")
print(f" Values in top 10% (>3686): {np.sum(image > 3686)} pixels")
print(f" Values in bottom 10% (<410): {np.sum(image < 410)} pixels")

# Check for patterns (like half-image issues)
left_half = image[:, :width//2]
right_half = image[:, width//2:]
top_half = image[:height//2, :]
bottom_half = image[height//2:, :]

print(f"\nHalf-image analysis:")
print(f" Left half: mean={left_half.mean():.1f}, max={left_half.max()}")
print(f" Right half: mean={right_half.mean():.1f}, max={right_half.max()}")
print(f" Top half: mean={top_half.mean():.1f}, max={top_half.max()}")
print(f" Bottom half: mean={bottom_half.mean():.1f}, max={bottom_half.max()}")

# Look for row-by-row patterns
row_means = np.mean(image, axis=1)
col_means = np.mean(image, axis=0)

print(f"\nRow/Column patterns:")
print(f" Row means: min={row_means.min():.1f}, max={row_means.max():.1f}, std={row_means.std():.2f}")
print(f" Col means: min={col_means.min():.1f}, max={col_means.max():.1f}, std={col_means.std():.2f}")

# Save a small crop as text for inspection
crop = image[1000:1020, 1000:1020] # 20x20 crop from middle
print(f"\n20x20 sample from center (row 1000-1020, col 1000-1020):")
for row in crop:
print(" " + " ".join(f"{val:4d}" for val in row))

if __name__ == "__main__":
analyze_bayer_raw()
81 changes: 81 additions & 0 deletions demosaic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
import numpy as np
import cv2
from pathlib import Path

def read_bayer_buffer(filename, width, height, bits_per_pixel=12):
"""Read raw Bayer buffer from file."""
with open(filename, 'rb') as f:
data = f.read()

# For 12-bit data stored in 16-bit format
if bits_per_pixel == 12:
# Convert bytes to 16-bit values (little-endian)
raw_data = np.frombuffer(data, dtype=np.uint16)
# Mask to 12-bit values
raw_data = raw_data & 0x0FFF
else:
raw_data = np.frombuffer(data, dtype=np.uint8)

# Reshape to image dimensions
image = raw_data.reshape(height, width)
return image

def demosaic_bayer(bayer_image, pattern='RGGB'):
"""Demosaic Bayer pattern to RGB image."""
height, width = bayer_image.shape

# Convert to 8-bit for OpenCV processing (scale from 12-bit to 8-bit)
bayer_8bit = (bayer_image >> 4).astype(np.uint8)

# Convert based on Bayer pattern
if pattern == 'RGGB':
color_image = cv2.cvtColor(bayer_8bit, cv2.COLOR_BAYER_RG2RGB)
elif pattern == 'GRBG':
color_image = cv2.cvtColor(bayer_8bit, cv2.COLOR_BAYER_GR2RGB)
elif pattern == 'GBRG':
color_image = cv2.cvtColor(bayer_8bit, cv2.COLOR_BAYER_GB2RGB)
elif pattern == 'BGGR':
color_image = cv2.cvtColor(bayer_8bit, cv2.COLOR_BAYER_BG2RGB)
else:
raise ValueError(f"Unknown Bayer pattern: {pattern}")

return color_image

def try_all_patterns(bayer_image):
"""Try all Bayer patterns and save results."""
patterns = ['RGGB', 'GRBG', 'GBRG', 'BGGR']

for pattern in patterns:
try:
color_image = demosaic_bayer(bayer_image, pattern)
output_filename = f'demosaic_{pattern.lower()}.jpg'
cv2.imwrite(output_filename, color_image)
print(f"Saved {output_filename} using {pattern} pattern")
except Exception as e:
print(f"Failed to process {pattern}: {e}")

def main():
# Image parameters from the captured output (full resolution)
width = 2464
height = 2056
bits_per_pixel = 12

# Read the buffer
print("Reading Bayer buffer...")
bayer_image = read_bayer_buffer('buffer_dump.bin', width, height, bits_per_pixel)

print(f"Bayer image shape: {bayer_image.shape}")
print(f"Bayer image min/max values: {bayer_image.min()}/{bayer_image.max()}")

# Try all Bayer patterns
print("Trying all Bayer patterns...")
try_all_patterns(bayer_image)

# Also save the raw Bayer data as grayscale for inspection
bayer_8bit = (bayer_image >> 4).astype(np.uint8)
cv2.imwrite('bayer_raw.jpg', bayer_8bit)
print("Saved bayer_raw.jpg (raw Bayer data as grayscale)")

if __name__ == "__main__":
main()
Loading