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#[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#[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 self.output(ctx, PIN_IMAGE, value).await
146 }
147 }
148}
149
150#[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 self.output(ctx, PIN_IMAGE, value).await
200 }
201 }
202}
203
204#[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 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 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 self.output(ctx, PIN_IMAGE, value).await
274 }
275 }
276}
277
278#[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#[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}