Skip to main content

askit_std_agents/
image.rs

1#![cfg(feature = "image")]
2
3use std::sync::Arc;
4
5use agent_stream_kit::photon_rs::{self, PhotonImage};
6use agent_stream_kit::{
7    ASKit, Agent, AgentContext, AgentData, AgentError, AgentOutput, AgentSpec, AgentValue, AsAgent,
8    askit_agent, async_trait,
9};
10
11const CATEGORY: &str = "Std/Image";
12
13const PIN_FILENAME: &str = "filename";
14const PIN_IMAGE: &str = "image";
15const PIN_IMAGE_FILENAME: &str = "image_filename";
16const PIN_BLANK: &str = "blank";
17const PIN_NON_BLANK: &str = "non_blank";
18const PIN_CHANGED: &str = "changed";
19const PIN_UNCHANGED: &str = "unchanged";
20const PIN_RESULT: &str = "result";
21
22const CONFIG_ALMOST_BLACK_THRESHOLD: &str = "almost_black_threshold";
23const CONFIG_BLANK_THRESHOLD: &str = "blank_threshold";
24const CONFIG_SCALE: &str = "scale";
25const CONFIG_HEIGHT: &str = "height";
26const CONFIG_WIDTH: &str = "width";
27const CONFIG_THRESHOLD: &str = "threshold";
28
29// IsBlankImageAgent
30#[askit_agent(
31    title = "isBlank",
32    category = CATEGORY,
33    inputs = [PIN_IMAGE],
34    outputs = [PIN_BLANK, PIN_NON_BLANK],
35    integer_config(name = CONFIG_ALMOST_BLACK_THRESHOLD, default = 20),
36    integer_config(name = CONFIG_BLANK_THRESHOLD, default = 400)
37)]
38struct IsBlankImageAgent {
39    data: AgentData,
40}
41
42impl IsBlankImageAgent {
43    fn is_blank(
44        &self,
45        image: &PhotonImage,
46        almost_black_threshold: u8,
47        blank_threshold: u32,
48    ) -> bool {
49        let mut count = 0;
50        for pixel in image.get_raw_pixels() {
51            if pixel >= almost_black_threshold {
52                count += 1;
53            }
54            if count >= blank_threshold {
55                return false;
56            }
57        }
58        true
59    }
60}
61
62#[async_trait]
63impl AsAgent for IsBlankImageAgent {
64    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
65        Ok(Self {
66            data: AgentData::new(askit, id, spec),
67        })
68    }
69
70    async fn process(
71        &mut self,
72        ctx: AgentContext,
73        _pin: String,
74        value: AgentValue,
75    ) -> Result<(), AgentError> {
76        let config = self.configs()?;
77
78        if value.is_image() {
79            let image = value
80                .as_image()
81                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
82
83            let almost_black_threshold =
84                config.get_integer_or_default(CONFIG_ALMOST_BLACK_THRESHOLD) as u8;
85            let blank_threshold = config.get_integer_or_default(CONFIG_BLANK_THRESHOLD) as u32;
86
87            let is_blank = self.is_blank(&image, almost_black_threshold, blank_threshold);
88            if is_blank {
89                self.output(ctx, PIN_BLANK, value).await
90            } else {
91                self.output(ctx, PIN_NON_BLANK, value).await
92            }
93        } else {
94            Err(AgentError::InvalidValue(
95                "Input value is not an image".into(),
96            ))
97        }
98    }
99}
100
101// ResampleImageAgent
102
103#[askit_agent(
104    title = "Resize Image",
105    category = CATEGORY,
106    inputs = [PIN_IMAGE],
107    outputs = [PIN_IMAGE],
108    integer_config(name = CONFIG_WIDTH, default = 512),
109    integer_config(name = CONFIG_HEIGHT, default = 512)
110)]
111struct ResampleImageAgent {
112    data: AgentData,
113}
114
115#[async_trait]
116impl AsAgent for ResampleImageAgent {
117    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
118        Ok(Self {
119            data: AgentData::new(askit, id, spec),
120        })
121    }
122
123    async fn process(
124        &mut self,
125        ctx: AgentContext,
126        _pin: String,
127        value: AgentValue,
128    ) -> Result<(), AgentError> {
129        let config = self.configs()?;
130
131        if value.is_image() {
132            let image = value
133                .as_image()
134                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
135
136            let width = config.get_integer_or_default(CONFIG_WIDTH) as usize;
137            let height = config.get_integer_or_default(CONFIG_HEIGHT) as usize;
138
139            let resampled_image = photon_rs::transform::resample(&*image, width, height);
140
141            self.output(ctx, PIN_IMAGE, AgentValue::image(resampled_image))
142                .await
143        } else {
144            // Pass through non-image value
145            self.output(ctx, PIN_IMAGE, value).await
146        }
147    }
148}
149
150// ResizeImageAgent
151
152#[askit_agent(
153    title = "Resize Image",
154    category = CATEGORY,
155    inputs = [PIN_IMAGE],
156    outputs = [PIN_IMAGE],
157    integer_config(name = CONFIG_WIDTH, default = 512),
158    integer_config(name = CONFIG_HEIGHT, default = 512)
159)]
160struct ResizeImageAgent {
161    data: AgentData,
162}
163
164#[async_trait]
165impl AsAgent for ResizeImageAgent {
166    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
167        Ok(Self {
168            data: AgentData::new(askit, id, spec),
169        })
170    }
171
172    async fn process(
173        &mut self,
174        ctx: AgentContext,
175        _pin: String,
176        value: AgentValue,
177    ) -> Result<(), AgentError> {
178        let config = self.configs()?;
179
180        if value.is_image() {
181            let image = value
182                .as_image()
183                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
184
185            let width = config.get_integer_or_default(CONFIG_WIDTH) as u32;
186            let height = config.get_integer_or_default(CONFIG_HEIGHT) as u32;
187
188            let resized_image = photon_rs::transform::resize(
189                &*image,
190                width,
191                height,
192                photon_rs::transform::SamplingFilter::Nearest,
193            );
194
195            self.output(ctx, PIN_IMAGE, AgentValue::image(resized_image))
196                .await
197        } else {
198            // Pass through non-image value
199            self.output(ctx, PIN_IMAGE, value).await
200        }
201    }
202}
203
204// ScaleImageAgent
205
206#[askit_agent(
207    title = "Scale Image",
208    category = CATEGORY,
209    inputs = [PIN_IMAGE],
210    outputs = [PIN_IMAGE],
211    number_config(name = CONFIG_SCALE, default = 1.0)
212)]
213struct ScaleImageAgent {
214    data: AgentData,
215}
216
217#[async_trait]
218impl AsAgent for ScaleImageAgent {
219    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
220        Ok(Self {
221            data: AgentData::new(askit, id, spec),
222        })
223    }
224
225    async fn process(
226        &mut self,
227        ctx: AgentContext,
228        _pin: String,
229        value: AgentValue,
230    ) -> Result<(), AgentError> {
231        let config = self.configs()?;
232
233        if value.is_image() {
234            let image = value
235                .as_image()
236                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
237
238            let scale = config.get_number_or_default(CONFIG_SCALE);
239
240            if scale <= 0.0 {
241                return Err(AgentError::InvalidValue(
242                    "Scale factor must be greater than 0".into(),
243                ));
244            }
245
246            if scale == 1.0 {
247                // No scaling needed, pass through the original image
248                return self.output(ctx, PIN_IMAGE, value).await;
249            }
250
251            if scale < 1.0 {
252                let width = ((image.get_width() as f64) * scale) as u32;
253                let height = ((image.get_height() as f64) * scale) as u32;
254
255                let resized_image = photon_rs::transform::resize(
256                    &*image,
257                    width,
258                    height,
259                    photon_rs::transform::SamplingFilter::Nearest,
260                );
261                self.output(ctx, PIN_IMAGE, AgentValue::image(resized_image))
262                    .await
263            } else {
264                // scale > 1.0
265                let width = ((image.get_width() as f64) * scale) as usize;
266                let height = ((image.get_height() as f64) * scale) as usize;
267                let resampled_image = photon_rs::transform::resample(&*image, width, height);
268                self.output(ctx, PIN_IMAGE, AgentValue::image(resampled_image))
269                    .await
270            }
271        } else {
272            // Pass through non-image value
273            self.output(ctx, PIN_IMAGE, value).await
274        }
275    }
276}
277
278// IsChangedImageAgent
279#[askit_agent(
280    title = "isChanged",
281    category = CATEGORY,
282    inputs = [PIN_IMAGE],
283    outputs = [PIN_CHANGED, PIN_UNCHANGED],
284    number_config(name = CONFIG_THRESHOLD, default = 0.01)
285)]
286struct IsChangedImageAgent {
287    data: AgentData,
288    last_image: Option<Arc<PhotonImage>>,
289}
290
291impl IsChangedImageAgent {
292    fn images_are_different(&self, img1: &PhotonImage, img2: &PhotonImage, threshold: f32) -> bool {
293        let pixels1 = img1.get_raw_pixels();
294        let pixels2 = img2.get_raw_pixels();
295
296        if pixels1.len() != pixels2.len() {
297            return true;
298        }
299
300        let diff_threshold = (threshold * pixels1.len() as f32) as usize;
301        let mut diff_count = 0;
302        for (p1, p2) in pixels1.iter().zip(pixels2.iter()) {
303            if p1 != p2 {
304                diff_count += 1;
305            }
306            if diff_count > diff_threshold {
307                return true;
308            }
309        }
310
311        false
312    }
313}
314
315#[async_trait]
316impl AsAgent for IsChangedImageAgent {
317    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
318        Ok(Self {
319            data: AgentData::new(askit, id, spec),
320            last_image: None,
321        })
322    }
323
324    async fn process(
325        &mut self,
326        ctx: AgentContext,
327        _pin: String,
328        value: AgentValue,
329    ) -> Result<(), AgentError> {
330        let config = self.configs()?;
331
332        if value.is_image() {
333            let image = value
334                .as_image()
335                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
336
337            let threshold = config.get_number_or_default(CONFIG_THRESHOLD) as f32;
338
339            let is_changed = if let Some(last_image) = &self.last_image {
340                self.images_are_different(&last_image, &image, threshold)
341            } else {
342                true
343            };
344
345            if is_changed {
346                self.last_image = value.clone().into_image();
347                self.output(ctx, PIN_CHANGED, value).await
348            } else {
349                self.output(ctx, PIN_UNCHANGED, value).await
350            }
351        } else {
352            Err(AgentError::InvalidValue(
353                "Input value is not an image".into(),
354            ))
355        }
356    }
357}
358
359// native
360
361#[askit_agent(
362    title = "Open Image",
363    category = CATEGORY,
364    inputs = [PIN_FILENAME],
365    outputs = [PIN_IMAGE]
366)]
367struct OpenImageAgent {
368    data: AgentData,
369}
370
371#[async_trait]
372impl AsAgent for OpenImageAgent {
373    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
374        Ok(Self {
375            data: AgentData::new(askit, id, spec),
376        })
377    }
378
379    async fn process(
380        &mut self,
381        ctx: AgentContext,
382        _pin: String,
383        value: AgentValue,
384    ) -> Result<(), AgentError> {
385        let filename = value
386            .as_str()
387            .ok_or_else(|| AgentError::InvalidValue("Expected filename string".into()))?;
388        let img_path = std::path::Path::new(filename);
389
390        let image = photon_rs::native::open_image(img_path).map_err(|e| {
391            AgentError::InvalidValue(format!("Failed to open image {}: {}", filename, e))
392        })?;
393
394        self.output(ctx, PIN_IMAGE, AgentValue::image(image)).await
395    }
396}
397
398#[askit_agent(
399    title = "Save Image",
400    category = CATEGORY,
401    inputs = [PIN_IMAGE_FILENAME],
402    outputs = [PIN_RESULT]
403)]
404struct SaveImageAgent {
405    data: AgentData,
406}
407
408#[async_trait]
409impl AsAgent for SaveImageAgent {
410    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
411        Ok(Self {
412            data: AgentData::new(askit, id, spec),
413        })
414    }
415
416    async fn process(
417        &mut self,
418        ctx: AgentContext,
419        _pin: String,
420        value: AgentValue,
421    ) -> Result<(), AgentError> {
422        let Some(image) = value.get_image("image") else {
423            return Err(AgentError::InvalidValue(
424                "Expected image value under 'image' key".into(),
425            ));
426        };
427
428        let Some(filename) = value.get_str("filename") else {
429            return Err(AgentError::InvalidValue(
430                "Expected filename string under 'filename' key".into(),
431            ));
432        };
433
434        photon_rs::native::save_image((*image).clone(), std::path::Path::new(filename)).map_err(
435            |e| AgentError::InvalidValue(format!("Failed to save image {}: {}", filename, e)),
436        )?;
437
438        self.output(ctx, PIN_RESULT, AgentValue::unit()).await
439    }
440}