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#[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#[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 self.try_output(ctx, PIN_IMAGE, value)
145 }
146 }
147}
148
149#[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 self.try_output(ctx, PIN_IMAGE, value)
198 }
199 }
200}
201
202#[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 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 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 self.try_output(ctx, PIN_IMAGE, value)
270 }
271 }
272}
273
274#[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#[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}