Bildverarbeitung in Rust mit Parallelität und Rayon optimieren
Verarbeiten Sie mit Rayon einzelne Bilder parallel, mit einem Job zum Decodieren, Skalieren und Codieren pro Worker. Diese Anleitung entwickelt eine Rust-CLI, die ein Verzeichnis mit PNG- und JPEG-Bildern in kleinere JPEGs umwandelt. Sie wählen die Worker-Anzahl, und der Befehl meldet jeden Dateifehler, bevor er seinen abschließenden Status zurückgibt.
Bild- und Ausgaberichtlinien wählen
Das Beispiel ist für lokale Stapel von Standbildern vorgesehen, mit diesen Festlegungen:
- Wählen Sie reguläre Dateien mit den Endungen
.png,.jpgoder.jpeg, unabhängig von der Groß- und Kleinschreibung der Endung. Überspringen Sie Unterverzeichnisse, symbolische Links und andere Endungen. Ändern Sie das Eingabeverzeichnis während eines Durchlaufs nicht. - Akzeptieren Sie Bilder, die in 8-Bit-RGB oder Graustufen decodiert werden, jeweils auch mit Alpha. Verwenden Sie sRGB-Eingaben: Das Programm konvertiert keine eingebetteten Farbprofile. Animationen und Druckworkflows mit Farbmanagement sind nicht Teil dieses Beispiels.
- Wenden Sie die EXIF-Ausrichtung des Decoders an, reduzieren Sie Transparenz auf Weiß und passen Sie das Bild dann in 800 × 600 Pixel ein, ohne es zuzuschneiden oder kleinere Bilder zu vergrößern. Codieren Sie JPEG mit Qualität 85.
- Hängen Sie
.jpgan den gesamten Eingabedateinamen an: Ausphoto.pngwirdphoto.png.jpg, und ausphoto.jpgwirdphoto.jpg.jpg. So bleiben Eingaben mit gleichem Basisnamen unterscheidbar. - Verlangen Sie ein neues Ausgabeverzeichnis. Ein vorhandenes Verzeichnis gilt als Fehler, auch wenn es leer ist. Erfolgreich erstellte Dateien bleiben erhalten, wenn eine andere Datei fehlschlägt; ein erneuter Durchlauf benötigt ein anderes Ausgabeverzeichnis.
Der Encoder erhält nur Pixel. EXIF-, GPS-, ICC- und Textmetadaten der Quelle werden daher nicht kopiert. Die Ausrichtung wird auf die Pixel angewendet, bevor diese Metadaten verworfen werden. Alpha-Compositing und Größenänderung arbeiten hier mit codierten Kanalwerten, nicht mit Werten im linearen Lichtraum. Das ist eine bewusste Vereinfachung für Thumbnails, keine Farbmanagement-Pipeline.
Das Cargo-Projekt erstellen
Die folgenden Befehle verwenden Bash unter Linux und wurden mit Rust und Cargo 1.98.1 getestet.
Installieren Sie die aktuelle stabile Toolchain mithilfe der
offiziellen Rust-Installationsanleitung.
Verwenden Sie ein Arbeitsverzeichnis außerhalb eines bestehenden Cargo-Projekts und seiner
Konfiguration .cargo. Alle folgenden Shell-Blöcke werden in demselben
Arbeitsverzeichnis eingefügt.
rustc --version && cargo --version
Erstellen Sie ein neues Verzeichnis; ein vorhandenes image-batch wird dabei nicht wiederverwendet:
(mkdir image-batch && mkdir image-batch/src)
Speichern Sie dieses vollständige Manifest als image-batch/Cargo.toml:
[package]
name = "image-batch"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "=1.0.104"
image = { version = "=0.25.10", default-features = false, features = ["jpeg", "png"] }
rayon = "=1.12.0"
[workspace]
Nur PNG- und JPEG-Codecs sind aktiviert. Rayon übernimmt die Parallelisierung des Stapels; das
optionale Rayon-Feature der Crate image ist deaktiviert. Die
Versionshinweise zu image beschreiben die hier verwendeten aktuellen
APIs für Decodierung und Ausrichtung.
Den vollständigen Befehl zur Stapelverarbeitung implementieren
Speichern Sie Folgendes als image-batch/src/main.rs. Die Verzeichniserfassung wird abgeschlossen,
bevor das Ausgabeverzeichnis erstellt wird. Rusts
read_dir kann sowohl beim Öffnen eines
Verzeichnisses als auch beim Weiterschalten seines Iterators fehlschlagen. Beide Fehler werden daher
weitergegeben, statt den Stapel stillschweigend zu verkleinern.
use std::{env, fs, path::Path, process::ExitCode};
use anyhow::{Context, Result, ensure};
use image::{ColorType, DynamicImage, ImageDecoder, ImageReader, Limits, Rgb, RgbImage};
use image::codecs::jpeg::JpegEncoder;
use image::imageops::FilterType;
use rayon::prelude::*;
fn convert(input: &Path, output_dir: &Path) -> Result<()> {
let mut reader = ImageReader::open(input)?.with_guessed_format()?;
let mut limits = Limits::default();
limits.max_image_width = Some(4096);
limits.max_image_height = Some(4096);
limits.max_alloc = Some(128 * 1024 * 1024);
reader.limits(limits);
let mut decoder = reader.into_decoder().context("Read image header")?;
ensure!(
matches!(decoder.color_type(), ColorType::L8 | ColorType::La8 | ColorType::Rgb8 | ColorType::Rgba8),
"Expected an 8-bit image"
);
let orientation = decoder.orientation()?;
let mut decoded = DynamicImage::from_decoder(decoder).context("Decode pixels")?;
decoded.apply_orientation(orientation);
let rgba = decoded.into_rgba8();
let rgb = RgbImage::from_fn(rgba.width(), rgba.height(), |x, y| {
let pixel = rgba.get_pixel(x, y);
let alpha = u16::from(pixel[3]);
let blend = |channel: u8| {
((u16::from(channel) * alpha + 255 * (255 - alpha) + 127) / 255) as u8
};
Rgb([blend(pixel[0]), blend(pixel[1]), blend(pixel[2])])
});
drop(rgba);
let image = DynamicImage::ImageRgb8(rgb);
let resized = if image.width() > 800 || image.height() > 600 {
image.resize(800, 600, FilterType::Lanczos3)
} else {
image
};
let mut bytes = Vec::new();
JpegEncoder::new_with_quality(&mut bytes, 85).encode_image(&resized)?;
let mut name = input.file_name().context("Missing filename")?.to_os_string();
name.push(".jpg");
let output = output_dir.join(&name);
name.push(".part");
let staging = output_dir.join(name);
fs::write(&staging, bytes).context("Write staged JPEG")?;
fs::rename(&staging, &output).context("Publish JPEG")?;
Ok(())
}
fn run() -> Result<()> {
let args: Vec<_> = env::args_os().skip(1).collect();
ensure!(args.len() == 3, "Usage: image-batch INPUT_DIR NEW_OUTPUT_DIR THREADS");
let threads: usize = args[2].to_str().context("Invalid thread count")?.parse()?;
ensure!((1..=16).contains(&threads), "THREADS must be between 1 and 16");
let input_dir = fs::canonicalize(&args[0]).context("Resolve input directory")?;
let mut inputs = Vec::new();
for entry in fs::read_dir(&input_dir).context("Open input directory")? {
let entry = entry.context("Read directory entry")?;
if !entry.file_type().context("Read entry type")?.is_file() {
continue;
}
let path = entry.path();
let supported = path.extension().is_some_and(|ext| {
ext.eq_ignore_ascii_case("png")
|| ext.eq_ignore_ascii_case("jpg")
|| ext.eq_ignore_ascii_case("jpeg")
});
if supported {
inputs.push(path);
}
}
ensure!(!inputs.is_empty(), "No PNG or JPEG files found");
inputs.sort();
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build()?;
let output_dir = Path::new(&args[1]);
fs::create_dir(output_dir).context("Create a new output directory; parent must exist")?;
let failed: usize = pool.install(|| {
inputs.par_iter().map(|input| {
match convert(input, output_dir) {
Ok(()) => 0,
Err(error) => {
eprintln!("FAILED {}: {error:#}", input.display());
1
}
}
}).sum()
});
println!("{} succeeded; {failed} failed", inputs.len() - failed);
ensure!(failed == 0, "Batch incomplete; successful outputs were kept");
Ok(())
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error:#}");
ExitCode::FAILURE
}
}
}
Jeder Job codiert das Bild, bevor er eine Datei mit der Endung .part schreibt,
und benennt diese anschließend in ihren endgültigen JPEG-Namen um. Nur eine abgeschlossene
Umbenennung zählt als Erfolg. Ein Schreibfehler oder eine Unterbrechung kann Dateien mit der Endung
.part hinterlassen; sie sind unvollständige Ergebnisse, keine veröffentlichten
Bilder. Halten Sie dieses Ausgabeverzeichnis exklusiv für den Durchlauf frei. Das Beispiel
synchronisiert Dateien nicht mit dauerhaftem Speicher und macht erfolgreiche Dateiausgaben nicht
rückgängig.
Der parallele Iterator zählt Fehler, statt beim ersten Fehler abzubrechen. Bei einem gewöhnlichen Dateifehler wird so weiterhin jedes gefundene Bild verarbeitet, während der Prozessstatus für einen unvollständigen Stapel ungleich null bleibt. Fehlermeldungen können in beliebiger Reihenfolge erscheinen.
Kompilieren, ausführen und eine Ausgabe prüfen
Erzeugen Sie image-batch/Cargo.lock einmal und bewahren Sie die Datei beim Projekt auf.
Exakte Versionsbindungen direkter Abhängigkeiten allein fixieren keine transitiven Abhängigkeiten;
spätere Builds verwenden die aufbewahrte Lock-Datei.
(cd image-batch && cargo generate-lockfile)
Installieren Sie für eine reproduzierbare Eingabe ImageMagick, hier mit 7.1.2-31 getestet. Damit erstellen Sie ein PNG mit 1.600 × 800, dessen linke Hälfte undurchsichtig rot und dessen rechte Hälfte transparent ist. Verwenden Sie für das Beispiel neue Verzeichnisnamen:
(
mkdir input-images &&
magick -size 1600x800 xc:none -fill red -draw 'rectangle 0,0 799,799' \
PNG32:input-images/sample.png
)
Kompilieren Sie im Release-Modus und führen Sie das Programm mit zwei Workern aus. Der Befehl startet die ausführbare Datei erst nach erfolgreichem Build:
(
cd image-batch &&
cargo build --release --locked &&
./target/release/image-batch ../input-images ../output-images 2
)
Für diese einzelne Eingabe meldet das Programm 1 succeeded; 0 failed. Prüfen Sie die
tatsächlich erzeugte Datei mit einem unabhängigen Decoder:
magick ./output-images/sample.png.jpg -format '%m %wx%h\n' info:
JPEG 800x400
Öffnen Sie auch das JPEG: Die linke Hälfte sollte rot und die rechte Hälfte weiß sein, einschließlich
des unteren Rands. resize bewahrt das
Seitenverhältnis innerhalb des Rahmens; es zwingt dieses Bild nicht auf 800 × 600. Die explizite
Größenprüfung belässt eine kleinere Eingabe bei ihren ursprünglichen Abmessungen. Das Reduzieren der
Transparenz vor der Größenänderung verhindert, dass verborgene RGB-Werte transparenter Pixel in den
weißen Hintergrund einfließen.
Um eigene Bilder zu verarbeiten, ersetzen Sie die beiden Verzeichnisargumente und verwenden Sie stets ein neues Ausgabeverzeichnis. Setzen Sie Pfade mit Leerzeichen in Anführungszeichen. Eine beschädigte ausgewählte Datei wird gemeldet, wenn der Decoder sie ablehnt; nicht unterstützte Endungen werden übersprungen. Eine erfolgreiche Decodierung ist keine Integritätsprüfung: Bei diesem Decoder kann ein abgeschnittenes JPEG erfolgreich decodiert werden, wobei graue Blöcke die fehlenden Pixel ersetzen. Prüfen Sie den Bildinhalt auf der gesamten Bildfläche und bewahren Sie Ihre Originale auf.
Worker-Anzahl im Verhältnis zu Speicherbedarf und Laufzeit messen
num_threads
begrenzt die Worker-Anzahl dieses Pools. Die CLI akzeptiert einen bis 16 Worker; beginnen Sie bei
großen Eingaben mit einem oder zwei. Sie speichert die Pfade des Verzeichnisses und hält decodierte
Pixel anschließend nur für aktive Jobs vor. Jeder Job kann mehrere Pixelpuffer benötigen, sodass eine
Verdopplung der Worker-Anzahl den Speicherbedarf erheblich erhöhen kann.
Die Grenzen von 4.096 Pixeln für Breite und Höhe gelten für die Quelle vor Ausrichtung und Größenänderung. Das Limit von 128 MiB für Speicherzuweisungen des Decoders gilt nach dem Best-Effort-Prinzip und umfasst weder die späteren RGBA/RGB-Puffer noch Größenänderung, JPEG-Encoding, Pfadliste oder den gesamten Prozessspeicher. Diese Maßnahmen machen das Programm nicht zu einer Sandbox für nicht vertrauenswürdige Uploads.
Vergleichen Sie nach einem erfolgreichen Build dieselbe repräsentative Eingabe mit mehreren Bildern und jeweils neuen Zielverzeichnissen:
time ./image-batch/target/release/image-batch ./input-images ./timed-one 1 &&
time ./image-batch/target/release/image-batch ./input-images ./timed-two 2
Vergleichen Sie die von Bash unter real angegebene Laufzeit, wiederholen Sie
den Test mit neuen Ausgabeverzeichnisnamen und berücksichtigen Sie bereits gefüllte Dateisystem-Caches.
Der Kurztest mit einem Bild kann keine Beschleunigung der Stapelverarbeitung nachweisen. Mehr Worker
können helfen, wenn unabhängiges Decodieren und Skalieren die CPU-Kerne auslasten; Speicher-I/O,
Speicherdruck und Scheduling-Overhead können den Gewinn aufheben. Behalten Sie Lanczos3 bei, wenn das
Ergebnis zu Ihren Bildern passt; vergleichen Sie einen anderen Filter mit denselben Eingaben, bevor
Sie Bildqualität zugunsten kürzerer Laufzeit aufgeben.
