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
11static CATEGORY: &str = "Std/Image";
12
13static PIN_FILENAME: &str = "filename";
14static PIN_IMAGE: &str = "image";
15static PIN_IMAGE_FILENAME: &str = "image_filename";
16static PIN_BLANK: &str = "blank";
17static PIN_NON_BLANK: &str = "non_blank";
18static PIN_CHANGED: &str = "changed";
19static PIN_UNCHANGED: &str = "unchanged";
20static PIN_RESULT: &str = "result";
21
22static CONFIG_ALMOST_BLACK_THRESHOLD: &str = "almost_black_threshold";
23static CONFIG_BLANK_THRESHOLD: &str = "blank_threshold";
24static CONFIG_SCALE: &str = "scale";
25static CONFIG_HEIGHT: &str = "height";
26static CONFIG_WIDTH: &str = "width";
27static 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.try_output(ctx, PIN_BLANK, value)
90            } else {
91                self.try_output(ctx, PIN_NON_BLANK, value)
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.try_output(ctx, PIN_IMAGE, AgentValue::image(resampled_image))
142        } else {
143            // Pass through non-image value
144            self.try_output(ctx, PIN_IMAGE, value)
145        }
146    }
147}
148
149// ResizeImageAgent
150
151#[askit_agent(
152    title = "Resize Image",
153    category = CATEGORY,
154    inputs = [PIN_IMAGE],
155    outputs = [PIN_IMAGE],
156    integer_config(name = CONFIG_WIDTH, default = 512),
157    integer_config(name = CONFIG_HEIGHT, default = 512)
158)]
159struct ResizeImageAgent {
160    data: AgentData,
161}
162
163#[async_trait]
164impl AsAgent for ResizeImageAgent {
165    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
166        Ok(Self {
167            data: AgentData::new(askit, id, spec),
168        })
169    }
170
171    async fn process(
172        &mut self,
173        ctx: AgentContext,
174        _pin: String,
175        value: AgentValue,
176    ) -> Result<(), AgentError> {
177        let config = self.configs()?;
178
179        if value.is_image() {
180            let image = value
181                .as_image()
182                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
183
184            let width = config.get_integer_or_default(CONFIG_WIDTH) as u32;
185            let height = config.get_integer_or_default(CONFIG_HEIGHT) as u32;
186
187            let resized_image = photon_rs::transform::resize(
188                &*image,
189                width,
190                height,
191                photon_rs::transform::SamplingFilter::Nearest,
192            );
193
194            self.try_output(ctx, PIN_IMAGE, AgentValue::image(resized_image))
195        } else {
196            // Pass through non-image value
197            self.try_output(ctx, PIN_IMAGE, value)
198        }
199    }
200}
201
202// ScaleImageAgent
203
204#[askit_agent(
205    title = "Scale Image",
206    category = CATEGORY,
207    inputs = [PIN_IMAGE],
208    outputs = [PIN_IMAGE],
209    number_config(name = CONFIG_SCALE, default = 1.0)
210)]
211struct ScaleImageAgent {
212    data: AgentData,
213}
214
215#[async_trait]
216impl AsAgent for ScaleImageAgent {
217    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
218        Ok(Self {
219            data: AgentData::new(askit, id, spec),
220        })
221    }
222
223    async fn process(
224        &mut self,
225        ctx: AgentContext,
226        _pin: String,
227        value: AgentValue,
228    ) -> Result<(), AgentError> {
229        let config = self.configs()?;
230
231        if value.is_image() {
232            let image = value
233                .as_image()
234                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
235
236            let scale = config.get_number_or_default(CONFIG_SCALE);
237
238            if scale <= 0.0 {
239                return Err(AgentError::InvalidValue(
240                    "Scale factor must be greater than 0".into(),
241                ));
242            }
243
244            if scale == 1.0 {
245                // No scaling needed, pass through the original image
246                return self.try_output(ctx, PIN_IMAGE, value);
247            }
248
249            if scale < 1.0 {
250                let width = ((image.get_width() as f64) * scale) as u32;
251                let height = ((image.get_height() as f64) * scale) as u32;
252
253                let resized_image = photon_rs::transform::resize(
254                    &*image,
255                    width,
256                    height,
257                    photon_rs::transform::SamplingFilter::Nearest,
258                );
259                self.try_output(ctx, PIN_IMAGE, AgentValue::image(resized_image))
260            } else {
261                // scale > 1.0
262                let width = ((image.get_width() as f64) * scale) as usize;
263                let height = ((image.get_height() as f64) * scale) as usize;
264                let resampled_image = photon_rs::transform::resample(&*image, width, height);
265                self.try_output(ctx, PIN_IMAGE, AgentValue::image(resampled_image))
266            }
267        } else {
268            // Pass through non-image value
269            self.try_output(ctx, PIN_IMAGE, value)
270        }
271    }
272}
273
274// IsChangedImageAgent
275#[askit_agent(
276    title = "isChanged",
277    category = CATEGORY,
278    inputs = [PIN_IMAGE],
279    outputs = [PIN_CHANGED, PIN_UNCHANGED],
280    number_config(name = CONFIG_THRESHOLD, default = 0.01)
281)]
282struct IsChangedImageAgent {
283    data: AgentData,
284    last_image: Option<Arc<PhotonImage>>,
285}
286
287impl IsChangedImageAgent {
288    fn images_are_different(&self, img1: &PhotonImage, img2: &PhotonImage, threshold: f32) -> bool {
289        let pixels1 = img1.get_raw_pixels();
290        let pixels2 = img2.get_raw_pixels();
291
292        if pixels1.len() != pixels2.len() {
293            return true;
294        }
295
296        let diff_threshold = (threshold * pixels1.len() as f32) as usize;
297        let mut diff_count = 0;
298        for (p1, p2) in pixels1.iter().zip(pixels2.iter()) {
299            if p1 != p2 {
300                diff_count += 1;
301            }
302            if diff_count > diff_threshold {
303                return true;
304            }
305        }
306
307        false
308    }
309}
310
311#[async_trait]
312impl AsAgent for IsChangedImageAgent {
313    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
314        Ok(Self {
315            data: AgentData::new(askit, id, spec),
316            last_image: None,
317        })
318    }
319
320    async fn process(
321        &mut self,
322        ctx: AgentContext,
323        _pin: String,
324        value: AgentValue,
325    ) -> Result<(), AgentError> {
326        let config = self.configs()?;
327
328        if value.is_image() {
329            let image = value
330                .as_image()
331                .ok_or_else(|| AgentError::InvalidValue("Expected image value".into()))?;
332
333            let threshold = config.get_number_or_default(CONFIG_THRESHOLD) as f32;
334
335            let is_changed = if let Some(last_image) = &self.last_image {
336                self.images_are_different(&last_image, &image, threshold)
337            } else {
338                true
339            };
340
341            if is_changed {
342                self.last_image = Some(image.clone());
343                self.try_output(ctx, PIN_CHANGED, value)
344            } else {
345                self.try_output(ctx, PIN_UNCHANGED, value)
346            }
347        } else {
348            Err(AgentError::InvalidValue(
349                "Input value is not an image".into(),
350            ))
351        }
352    }
353}
354
355// native
356
357#[askit_agent(
358    title = "Open Image",
359    category = CATEGORY,
360    inputs = [PIN_FILENAME],
361    outputs = [PIN_IMAGE]
362)]
363struct OpenImageAgent {
364    data: AgentData,
365}
366
367#[async_trait]
368impl AsAgent for OpenImageAgent {
369    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
370        Ok(Self {
371            data: AgentData::new(askit, id, spec),
372        })
373    }
374
375    async fn process(
376        &mut self,
377        ctx: AgentContext,
378        _pin: String,
379        value: AgentValue,
380    ) -> Result<(), AgentError> {
381        let filename = value
382            .as_str()
383            .ok_or_else(|| AgentError::InvalidValue("Expected filename string".into()))?;
384        let img_path = std::path::Path::new(filename);
385
386        let image = photon_rs::native::open_image(img_path).map_err(|e| {
387            AgentError::InvalidValue(format!("Failed to open image {}: {}", filename, e))
388        })?;
389
390        self.try_output(ctx, PIN_IMAGE, AgentValue::image(image))
391    }
392}
393
394#[askit_agent(
395    title = "Save Image",
396    category = CATEGORY,
397    inputs = [PIN_IMAGE_FILENAME],
398    outputs = [PIN_RESULT]
399)]
400struct SaveImageAgent {
401    data: AgentData,
402}
403
404#[async_trait]
405impl AsAgent for SaveImageAgent {
406    fn new(askit: ASKit, id: String, spec: AgentSpec) -> Result<Self, AgentError> {
407        Ok(Self {
408            data: AgentData::new(askit, id, spec),
409        })
410    }
411
412    async fn process(
413        &mut self,
414        ctx: AgentContext,
415        _pin: String,
416        value: AgentValue,
417    ) -> Result<(), AgentError> {
418        let Some(image) = value.get_image("image") else {
419            return Err(AgentError::InvalidValue(
420                "Expected image value under 'image' key".into(),
421            ));
422        };
423
424        let Some(filename) = value.get_str("filename") else {
425            return Err(AgentError::InvalidValue(
426                "Expected filename string under 'filename' key".into(),
427            ));
428        };
429
430        photon_rs::native::save_image((*image).clone(), std::path::Path::new(filename)).map_err(
431            |e| AgentError::InvalidValue(format!("Failed to save image {}: {}", filename, e)),
432        )?;
433
434        self.try_output(ctx, PIN_RESULT, AgentValue::unit())
435    }
436}