Raspberry pi + camera

Has anyone successfully used rust on the raspberry pi + raspberry pi camera to take photos?

My goal is to just take a picture.

All the crates (raspicam, rascam)I've tried run into the same issue of relying on mmal but that has been removed from raspberry pi for a long time.

I'm currently seeing if I can use the libcamera bindings.

Would love pointers, I don't know much about writing my own bindings and have no idea what to do next.

1 Like

Even simpler: if you have libcamera-still (cli) you should be able to call it with:

use std::process::Command;

fn main() {
    let status = Command::new("libcamera-still")
        .args(&["-o", "image.jpg", "--immediate"])
        .status()
        .expect("failed to run libcamera-still");

    if status.success() {
        println!("Image captured successfully!");
    } else {
        eprintln!("libcamera-still failed");
    }
}

That is the current solution I have!

But it's ugly and spawns a new process to do so.
It's good enough for now I just wanted to get my hands a little dirtier into the rust.

You will need libcamera. And current 0.4 is quite "raw". Following is the code I am using:

First, make sure of the camera id.

pub async fn camera_scan() -> Result<()> {
    let cam_mgr = CameraManager::new()?;
    let cams = cam_mgr.cameras();
    for i in 0..cams.len() {
        let cam = cams
            .get(i)
            .ok_or(eyre!("Cam {i} is gone when fetching from list"))?;
        println!("{}\t{}", i, cam.id());
        println!("{:?}", cam.properties());
        let cfgs = cam
            .generate_configuration(&[StreamRole::StillCapture])
            .ok_or(eyre!("Cannot get configuration from camera {}", cam.id()))?;
        for j in 0..cfgs.len() {
            println!("{i}.{j} available formats:");
            println!(
                "{:?}",
                cfgs.get(j)
                    .ok_or(eyre!(
                        "Cam {i} configration {j} is gone when fetching from list"
                    ))?
                    .formats()
            );
        }
    }
    Ok(())
}

Next taking the pics is quite tricky. I need an async env, hence following may contain "seems unnecessary" stuff:

    let shutter_button = Arc::new((Mutex::new(false), Condvar::new()));
    let (photo_tx, mut photo_rx) = mpsc::channel(4);
    let trigger = shutter_button.clone();
    let x = camera_id.to_owned();
    spawn_blocking(move || {
        if let Err(e) = camera_thread(&x, trigger, photo_tx) {
            tracing::error!("{e:?}");
        }
    })
    .await?;

        let (_, v) = &*shutter_button;
        v.notify_one();
        let photo_request_time = Utc::now();
        let image = photo_rx
            .recv()
            .await
            .ok_or(eyre!("Response channel closed"))?;

The image above is a DynamicImage from image crate. You can directly save it as some format or in my case, handed to OpenCV.

fn camera_thread(
    camera_id: &str,
    shutter_button: Arc<(Mutex<bool>, Condvar)>,
    photo_tx: Sender<DynamicImage>,
) -> Result<()> {
    let cam_mgr = CameraManager::new()?;
    let cams = cam_mgr.cameras();
    let mut chosen = None;
    for i in 0..cams.len() {
        let cam = cams
            .get(i)
            .ok_or(eyre!("Cam {i} is gone when fetching from list"))?;
        if cam.id() == camera_id {
            chosen = Some(cam);
            break;
        }
    }
    let cam = chosen.ok_or(eyre!("Could not find specified camera"))?;
    let mut cam = cam.acquire()?;

    let (tx, rx) = channel::<Request>();
    cam.on_request_completed(move |req| {
        if let Err(e) = tx.send(req) {
            tracing::warn!("Camera thread internal channel error: {e:?}");
        }
    });

    let mut cfgs = cam
        .generate_configuration(&[StreamRole::StillCapture])
        .ok_or(eyre!("Cannot get configuration from camera {}", cam.id()))?;
    let mut cfg = cfgs.get_mut(0).unwrap();
    cfg.set_pixel_format(PIXEL_FORMAT_RGB888);
    let max_size = cfg.formats().range(PIXEL_FORMAT_RGB888).max;
    cfg.set_size(max_size);
    match cfgs.validate() {
        CameraConfigurationStatus::Valid => (),
        CameraConfigurationStatus::Adjusted => {
            // What should I do about this?
            tracing::warn!("Configuration was adjusted.")
        }
        CameraConfigurationStatus::Invalid => Err(eyre!("Invalid camera configuration."))?,
    }
    cam.configure(&mut cfgs)?;

    let x: f64 = <f32 as Into<f64>>::into(EATING_CAT_THRESHOLD)
        * <u32 as Into<f64>>::into(max_size.height)
        * <u32 as Into<f64>>::into(max_size.width);
    MIN_CAT_SIZE
        .set(x.round() as u64)
        .map_err(|_| eyre!("Could not init MIN_CAT_SIZE"))?;

    let mut alloc = FrameBufferAllocator::new(&cam);
    let cfg = cfgs.get(0).unwrap();
    let stream = cfg.stream().unwrap();
    // The buffers len alloced is actually 1. Maybe related to `role`?
    let fb = alloc
        .alloc(&stream)?
        .pop()
        .ok_or(eyre!("Could not allocate even one framebuffer"))?;
    let fb = MemoryMappedFrameBuffer::new(fb)?;
    let mut req = cam
        .create_request(None)
        .ok_or(eyre!("Cannot create request"))?;
    req.add_buffer(&stream, fb)?;

    // The first photo status is FrameStartup, sleeping 1 sec did not help.
    // So take a photo and drop it.
    cam.start(None)?;
    cam.queue_request(req)?;
    req = rx.recv_timeout(Duration::from_secs(2))?;
    cam.stop()?;

    loop {
        // Wait until the trigger
        let (l, v) = &*shutter_button;
        let _not_care = l
            .lock()
            .and_then(|l| v.wait(l))
            .map_err(|e| eyre!("{e:?}"))?;

        req.reuse(ReuseFlag::REUSE_BUFFERS);
        cam.start(None)?;
        cam.queue_request(req)?;
        req = rx.recv_timeout(Duration::from_secs(2))?;
        cam.stop()?;

        let fb: &MemoryMappedFrameBuffer<FrameBuffer> =
            req.buffer(&stream).expect("Unmatched buffer types");
        let plane = fb.data().pop().ok_or(eyre!("No photo got"))?; // only 1 plane.
        let metadata = fb.metadata().ok_or(eyre!("No metadata got"))?;
        if metadata.status() == FrameMetadataStatus::Success {
            // let data_length = metadata
            //     .planes()
            //     .get(0)
            //     .ok_or(eyre!("No metadata planes"))?
            //     .bytes_used;
            // tracing::debug!("{} x {} = {data_length}", max_size.height, max_size.width);

            let image = ImageBuffer::from_vec(max_size.width, max_size.height, plane.to_vec())
                .ok_or(eyre!("Cannot create image, input not big enough"))?;
            let image = DynamicImage::ImageRgb8(image);

            photo_tx.blocking_send(image)?;
        }
    }
}

Magicloud, could you be more specific with regard to your code. Specifically, are the code snippets all in one module or in separate modules; also, what does your .toml file contain for dependencies? Lastly, my current release of libcamera is 0.5.2 on my raspberry pi 4/5 (trixie/bookworm) and I built it from source as well as the rpicam-apps. Another approach is to use a python libcamera app and stdprocess as was mentioned, and it's performance is not bad, although I've not benchmarked it nor compared it to other approaches. I did try using libcamlite-rs to no avail, just couldn't get this crate to work for me and finally decided it wasn't worth pursuing any longer as a python app and stdprocess work OK. My libcamera/python app also has the capability to pass the image along to opencv for post capture processing, although it's still work in process. So far, I've just got the python app with libcamera providing a display.

are the code snippets all in one module or in separate modules

Yes. But I am not sure if that would affect the logic.

what does your .toml file contain for dependencies

It is quite a lot, but not all necessary for the function. libcamera should be enough. When I wrote the last answer, libcamera-rs was 0.4 working with libcamera 0.4/0.5. I saw some changes in its github repo may released as 0.5 with some breaking changes.

mimalloc = { version = "*" }
eyre = { version = "*" }
color-eyre = { version = "*" }
tracing-error = { version = "*" }
clap = { version = "*", features = ["derive"] }
# tokio = { version = "*", features = ["full"] }
smol = { version = "*" }
blocking = { version = "*" }
smol-macros = { version = "*" }
macro_rules_attribute = { version = "*" }
async-compat = { version = "*" }
crossfire = { version = "*", features = ["smol"] }
tracing = { version = "*" }
tracing-subscriber = { version = "*", features = ["env-filter"] }
libcamera = { version = "*" }
ort = { version = "2.0.0-rc.10", default-features = false, features = [] }
image = { version = "*", default-features = false, features = ["webp"] }
yolo-rs = { git = "https://github.com/Magicloud/yolo-rs" }
# num = { version = "*" }
# float-cmp = { version = "*" }
gethostname = { version = "*" }
tonic = { version = "*" }
prost = { version = "*" }
tonic-prost = { version = "*" }
chrono = { version = "*" }
futures-core = { version = "*" }
# surrealdb = { version = "*" }
gql_client = { version = "*" }
serde = { version = "*", features = ["derive"] }
actix-web = { version = "*" }
actix-files = { version = "*" }
# regex = { version = "*" }