MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VisionExtractor.cpp
Go to the documentation of this file.
1#include "VisionExtractor.hpp"
2
8
9#include <numeric>
10
11namespace MayaFlux::Yantra {
12
13namespace {
14
15 using Direction = GpuBufferBinding::Direction;
16 using ElementType = GpuBufferBinding::ElementType;
18
19 struct CropPC {
21 uint32_t out_w, out_h;
22 };
23
24 struct PatchExtractPC {
26 };
27
28 struct AtlasPC {
29 uint32_t tile_w, tile_h, count;
30 };
31
32 struct BatchCountPC {
33 uint32_t count;
34 };
35
36 struct PointSamplePC {
37 uint32_t count, radius, src_w, src_h;
38 };
39
40 struct SelectPC {
42 float px, py;
43 };
44
45 struct TonePC {
46 uint32_t mode;
47 float gain;
48 uint32_t width, height;
49 };
50
51 struct EdgePC {
52 float threshold;
53 uint32_t out_w, out_h;
54 };
55
56 struct MotionPC {
58 uint32_t out_w, out_h;
59 };
60
61 struct SegmentsPC {
62 uint32_t count, width, height;
63 };
64
65 constexpr std::array<uint32_t, 3> k_wg_image { 16, 16, 1 };
66 constexpr std::array<uint32_t, 3> k_wg_reduce { 256, 1, 1 };
67
68 [[nodiscard]] GpuBufferBinding binding_at(uint32_t binding, Direction direction, ElementType type)
69 {
70 return { .set = 0, .binding = binding, .direction = direction, .element_type = type };
71 }
72
73 /**
74 * @brief Shared context for the image-producing methods: output image at
75 * 0, source at 1, a second image at 2, input buffers at 3 to 5.
76 */
77 [[nodiscard]] std::shared_ptr<TextureExecutionContext> make_image_context()
78 {
79 return std::make_shared<TextureExecutionContext>(
80 GpuComputeConfig {},
81 std::vector<GpuBufferBinding> {
82 binding_at(0, Direction::OUTPUT, ElementType::IMAGE_STORAGE),
83 binding_at(1, Direction::INPUT, ElementType::IMAGE_SAMPLED),
84 binding_at(2, Direction::INPUT, ElementType::IMAGE_SAMPLED),
85 binding_at(3, Direction::INPUT, ElementType::FLOAT32),
86 binding_at(4, Direction::INPUT, ElementType::FLOAT32),
87 binding_at(5, Direction::INPUT, ElementType::FLOAT32),
88 },
89 OutputMode::IMAGE);
90 }
91
92 /**
93 * @brief Shared context for the methods that read numbers back: source at
94 * 1, input buffer at 3, output buffer at 6.
95 */
96 [[nodiscard]] std::shared_ptr<TextureExecutionContext> make_reduce_context()
97 {
98 return std::make_shared<TextureExecutionContext>(
99 GpuComputeConfig {},
100 std::vector<GpuBufferBinding> {
101 binding_at(1, Direction::INPUT, ElementType::IMAGE_SAMPLED),
102 binding_at(3, Direction::INPUT, ElementType::FLOAT32),
103 binding_at(6, Direction::OUTPUT, ElementType::FLOAT32),
104 },
105 OutputMode::SCALAR);
106 }
107
108 /**
109 * @brief Swap in a shader and drop whatever the previous call staged, so
110 * nothing stale is uploaded or transitioned for this one.
111 */
112 void use_shader(TextureExecutionContext& ctx, const char* shader, std::array<uint32_t, 3> workgroup, size_t push_size)
113 {
114 ctx.swap_shader({ .shader_path = shader, .workgroup_size = workgroup, .push_constant_size = push_size });
115 for (size_t b = 3; b <= 5; ++b)
116 ctx.set_binding_data(b, std::span<const float> {});
117 ctx.stage_image_at(1, nullptr, ElementType::IMAGE_SAMPLED);
118 ctx.stage_image_at(2, nullptr, ElementType::IMAGE_SAMPLED);
119 }
120
121 void dispatch_and_wait(TextureExecutionContext& ctx)
122 {
123 auto& foundry = Portal::Graphics::get_shader_foundry();
124 const auto fence = ctx.dispatch_async({});
125 ctx.clear_output_dimensions();
126 foundry.wait_for_fence(fence);
127 foundry.release_fence(fence);
128 }
129
130 [[nodiscard]] std::shared_ptr<Core::VKImage> dispatch_image(TextureExecutionContext& ctx, uint32_t w, uint32_t h)
131 {
132 ctx.prepare_output_image(w, h);
133 ctx.set_output_dimensions(w, h);
134 dispatch_and_wait(ctx);
135 return ctx.get_output_image(0);
136 }
137
138 /** @brief Run a reduce-context dispatch of x invocations and return output buffer 6 as floats. */
139 [[nodiscard]] std::vector<float> dispatch_reduce(TextureExecutionContext& ctx, uint32_t invocations, size_t output_bytes)
140 {
141 ctx.set_output_size(6, output_bytes);
142 ctx.set_output_dimensions(invocations, 1);
143 dispatch_and_wait(ctx);
144
145 const auto result = ctx.collect_result();
146 const auto it = result.aux.find(6);
147 if (it == result.aux.end())
148 return {};
149
150 std::vector<float> out(it->second.size() / sizeof(float));
151 std::memcpy(out.data(), it->second.data(), out.size() * sizeof(float));
152 return out;
153 }
154
155 [[nodiscard]] vk::Sampler default_sampler()
156 {
158 }
159
160 [[nodiscard]] Kinesis::Vision::FieldSample field_sample(const float* raw)
161 {
162 return {
163 .mean = { raw[0], raw[1], raw[2], raw[3] },
164 .min_val = { raw[4], raw[5], raw[6], raw[7] },
165 .max_val = { raw[8], raw[9], raw[10], raw[11] },
166 };
167 }
168
169 [[nodiscard]] std::array<uint32_t, 4> pixel_rect(const Kinesis::Vision::BoundingBox& box, uint32_t w, uint32_t h)
170 {
171 return {
172 static_cast<uint32_t>(box.x * static_cast<float>(w)),
173 static_cast<uint32_t>(box.y * static_cast<float>(h)),
174 std::max<uint32_t>(1U, static_cast<uint32_t>(box.w * static_cast<float>(w))),
175 std::max<uint32_t>(1U, static_cast<uint32_t>(box.h * static_cast<float>(h))),
176 };
177 }
178
179 void add_line(std::vector<float>& segments, glm::vec2 a, glm::vec2 b, glm::vec4 color)
180 {
181 segments.insert(segments.end(), { a.x, a.y, b.x, b.y, color.r, color.g, color.b, color.a });
182 }
183
184 void add_rect(std::vector<float>& segments, const Kinesis::Vision::BoundingBox& box, glm::vec4 color)
185 {
186 const glm::vec2 tl { box.x, box.y };
187 const glm::vec2 tr { box.x + box.w, box.y };
188 const glm::vec2 br { box.x + box.w, box.y + box.h };
189 const glm::vec2 bl { box.x, box.y + box.h };
190 add_line(segments, tl, tr, color);
191 add_line(segments, tr, br, color);
192 add_line(segments, br, bl, color);
193 add_line(segments, bl, tl, color);
194 }
195
196 void add_cross(std::vector<float>& segments, glm::vec2 p, glm::vec2 half, glm::vec4 color)
197 {
198 add_line(segments, { p.x - half.x, p.y }, { p.x + half.x, p.y }, color);
199 add_line(segments, { p.x, p.y - half.y }, { p.x, p.y + half.y }, color);
200 }
201
202} // namespace
203
205 : Base([this](const input_type& input) { return run_operation(input); })
206 , m_mode(mode)
207{
208}
209
211{
212 if (!m_image_ctx)
213 m_image_ctx = make_image_context();
214 return *m_image_ctx;
215}
216
218{
219 if (!m_reduce_ctx)
220 m_reduce_ctx = make_reduce_context();
221 return *m_reduce_ctx;
222}
223
225{
226 if (!analysis.find_elements)
227 return std::nullopt;
228
229 const auto& elements = *analysis.find_elements;
230
231 if (m_focus)
232 return select_element(analysis, *m_focus);
233
234 if (m_element_index < elements.contours.size()) {
235 const auto label = elements.contours[m_element_index].label_id;
236 if (label != 0 && label <= elements.boxes.size())
237 return static_cast<size_t>(label - 1U);
238 }
239
240 if (m_element_index < elements.boxes.size())
241 return m_element_index;
242
243 return std::nullopt;
244}
245
246std::optional<Kinesis::Vision::BoundingBox> VisionExtractor::region_for_analysis(
247 const Kinesis::Vision::VisionAnalysis& analysis)
248{
249 if (const auto index = focus_box_index(analysis); index && *index < analysis.find_elements->boxes.size())
250 return analysis.find_elements->boxes[*index];
251 if (analysis.track_objects)
252 return analysis.track_objects->bounds;
253 if (analysis.detect_features)
254 return analysis.detect_features->bounds;
255 return std::nullopt;
256}
257
259 const Kinesis::Vision::VisionAnalysis& analysis)
260{
261 if (!analysis.find_elements)
262 return std::nullopt;
263
264 const auto& elements = *analysis.find_elements;
265
266 if (m_focus) {
267 const auto index = select_element(analysis, *m_focus);
268 if (!index)
269 return std::nullopt;
270 const auto label = elements.boxes[*index].label_id;
271 return label != 0 ? label : static_cast<uint32_t>(*index + 1U);
272 }
273
274 if (m_element_index >= elements.contours.size())
275 return std::nullopt;
276
277 const auto label = elements.contours[m_element_index].label_id;
278 if (label == 0)
279 return std::nullopt;
280 return label;
281}
282
284 const Kinesis::Vision::VisionAnalysis& analysis) const
285{
286 std::vector<glm::vec2> points;
287 if (analysis.detect_features) {
288 points.reserve(analysis.detect_features->keypoints.size());
289 for (const auto& k : analysis.detect_features->keypoints)
290 points.push_back(k.position);
291 } else if (analysis.track_objects) {
292 points.reserve(analysis.track_objects->tracks.size());
293 for (const auto& t : analysis.track_objects->tracks)
294 points.push_back(t.position);
295 }
296 return points;
297}
298
300 const Kinesis::Vision::VisionAnalysis& analysis,
301 const ElementFocus& focus)
302{
303 if (!analysis.find_elements || analysis.find_elements->boxes.empty())
304 return std::nullopt;
305
306 const auto& elements = *analysis.find_elements;
307 const auto count = elements.boxes.size();
308
309 std::vector<float> metrics;
310 metrics.reserve(count * 6);
311 for (size_t i = 0; i < count; ++i) {
312 const auto& box = elements.boxes[i];
313 const uint32_t label = box.label_id != 0 ? box.label_id : static_cast<uint32_t>(i + 1U);
314 const auto contour = std::ranges::find_if(elements.contours,
315 [label](const auto& c) { return c.label_id == label; });
316
317 metrics.push_back(contour != elements.contours.end() ? contour->area : box.w * box.h);
318 metrics.push_back(i < elements.shapes.size() ? elements.shapes[i].compactness : 0.0F);
319 metrics.push_back(i < elements.shapes.size() ? elements.shapes[i].aspect_ratio : 0.0F);
320 metrics.push_back(i < elements.nearest_neighbor_distance_px.size() ? elements.nearest_neighbor_distance_px[i] : 0.0F);
321 metrics.push_back(box.x + box.w * 0.5F);
322 metrics.push_back(box.y + box.h * 0.5F);
323 }
324
325 auto& ctx = reduce_context();
326 use_shader(ctx, "element_select.comp.spv", k_wg_reduce, sizeof(SelectPC));
327 ctx.set_binding_data(3, std::span<const float>(metrics));
328 ctx.set_push_constants(SelectPC {
329 .count = static_cast<uint32_t>(count),
330 .criterion = static_cast<uint32_t>(focus.criterion),
331 .largest = focus.largest ? 1U : 0U,
332 .px = focus.point.x,
333 .py = focus.point.y,
334 });
335
336 const auto raw = dispatch_reduce(ctx, 256, 2 * sizeof(float));
337 if (raw.empty())
338 return std::nullopt;
339
340 const auto index = static_cast<size_t>(std::lround(raw[0]));
341 return index < count ? std::optional<size_t> { index } : std::nullopt;
342}
343
344std::shared_ptr<Core::VKImage> VisionExtractor::crop(
345 const std::shared_ptr<Core::VKImage>& source,
346 const Kinesis::Vision::VisionAnalysis& analysis,
347 uint32_t out_w, uint32_t out_h)
348{
349 const auto region = region_for_analysis(analysis);
350 if (!region)
351 return nullptr;
352
353 auto& ctx = image_context();
354 use_shader(ctx, "vision_crop.comp.spv", k_wg_image, sizeof(CropPC));
355 ctx.stage_image(source);
356 ctx.set_push_constants(CropPC {
357 .src_x = region->x, .src_y = region->y, .src_w = region->w, .src_h = region->h,
358 .out_w = out_w, .out_h = out_h });
359 return dispatch_image(ctx, out_w, out_h);
360}
361
362std::shared_ptr<Core::VKImage> VisionExtractor::crops(
363 const std::shared_ptr<Core::VKImage>& source,
364 const Kinesis::Vision::VisionAnalysis& analysis,
365 uint32_t tile_w, uint32_t tile_h)
366{
367 if (!analysis.find_elements || analysis.find_elements->boxes.empty() || tile_w == 0 || tile_h == 0)
368 return nullptr;
369
370 const auto& boxes = analysis.find_elements->boxes;
371 const auto count = static_cast<uint32_t>(boxes.size());
372
373 std::vector<float> rects;
374 rects.reserve(boxes.size() * 4);
375 for (const auto& b : boxes)
376 rects.insert(rects.end(), { b.x, b.y, b.w, b.h });
377
378 auto& ctx = image_context();
379 use_shader(ctx, "vision_region_atlas.comp.spv", k_wg_image, sizeof(AtlasPC));
380 ctx.stage_image(source);
381 ctx.set_binding_data(3, std::span<const float>(rects));
382 ctx.set_push_constants(AtlasPC { .tile_w = tile_w, .tile_h = tile_h, .count = count });
383 return dispatch_image(ctx, tile_w * count, tile_h);
384}
385
386std::shared_ptr<Core::VKImage> VisionExtractor::mask(
387 VisionGpuExecutor& executor,
388 const std::shared_ptr<Core::VKImage>& source,
389 const Kinesis::Vision::VisionAnalysis& analysis,
390 uint32_t w, uint32_t h)
391{
392 const auto label = label_for_analysis(analysis);
393 if (!label)
394 return nullptr;
395
396 return executor.select_label(source, *label, w, h);
397}
398
399std::shared_ptr<Core::VKImage> VisionExtractor::fill_elements(
400 const std::shared_ptr<Core::VKImage>& source,
402 const std::vector<size_t>& indices,
403 uint32_t tile_w, uint32_t tile_h)
404{
405 if (indices.empty() || tile_w == 0 || tile_h == 0)
406 return nullptr;
407
408 std::vector<float> points;
409 std::vector<uint32_t> polygons;
410 std::vector<uint32_t> table;
411 table.reserve(indices.size() * 6);
412
413 for (const auto index : indices) {
414 const auto& box = elements.boxes[index];
415 const uint32_t label = box.label_id != 0 ? box.label_id : static_cast<uint32_t>(index + 1U);
416
417 const auto first_polygon = static_cast<uint32_t>(polygons.size() / 2);
418 uint32_t polygon_count = 0;
419 for (const auto& contour : elements.contours) {
420 const bool owned = contour.label_id == label || (contour.label_id == 0 && contour.parent_label == label);
421 if (!owned || contour.points.size() < 3)
422 continue;
423
424 polygons.push_back(static_cast<uint32_t>(points.size() / 2));
425 polygons.push_back(static_cast<uint32_t>(contour.points.size()));
426 for (const auto& p : contour.points)
427 points.insert(points.end(), { p.x, p.y });
428 ++polygon_count;
429 }
430
431 table.insert(table.end(), {
432 std::bit_cast<uint32_t>(box.x),
433 std::bit_cast<uint32_t>(box.y),
434 std::bit_cast<uint32_t>(box.w),
435 std::bit_cast<uint32_t>(box.h),
436 first_polygon,
437 polygon_count,
438 });
439 }
440
441 if (points.empty())
442 points.assign(2, 0.0F);
443 if (polygons.empty())
444 polygons.assign(2, 0U);
445
446 const auto count = static_cast<uint32_t>(indices.size());
447
448 auto& ctx = image_context();
449 use_shader(ctx, "contour_fill.comp.spv", k_wg_image, sizeof(AtlasPC));
450 ctx.stage_image(source);
451 ctx.set_binding_data(3, std::span<const float>(points));
452 ctx.set_binding_data(4, std::span<const uint32_t>(polygons));
453 ctx.set_binding_data(5, std::span<const uint32_t>(table));
454 ctx.set_push_constants(AtlasPC { .tile_w = tile_w, .tile_h = tile_h, .count = count });
455 return dispatch_image(ctx, tile_w * count, tile_h);
456}
457
458std::shared_ptr<Core::VKImage> VisionExtractor::silhouettes(
459 const std::shared_ptr<Core::VKImage>& source,
460 const Kinesis::Vision::VisionAnalysis& analysis,
461 uint32_t tile_w, uint32_t tile_h)
462{
463 if (!analysis.find_elements || analysis.find_elements->boxes.empty())
464 return nullptr;
465
466 std::vector<size_t> indices(analysis.find_elements->boxes.size());
467 std::iota(indices.begin(), indices.end(), size_t { 0 });
468 return fill_elements(source, *analysis.find_elements, indices, tile_w, tile_h);
469}
470
471std::shared_ptr<Core::VKImage> VisionExtractor::silhouette(
472 const std::shared_ptr<Core::VKImage>& source,
473 const Kinesis::Vision::VisionAnalysis& analysis,
474 uint32_t tile_w, uint32_t tile_h)
475{
476 const auto index = focus_box_index(analysis);
477 if (!index || *index >= analysis.find_elements->boxes.size())
478 return nullptr;
479
480 return fill_elements(source, *analysis.find_elements, { *index }, tile_w, tile_h);
481}
482
484 const std::shared_ptr<Core::VKImage>& source,
485 const Kinesis::Vision::VisionAnalysis& analysis,
486 uint32_t source_w, uint32_t source_h)
487{
488 const auto region = region_for_analysis(analysis);
489 if (!region)
490 return {};
491
492 const auto rect = pixel_rect(*region, source_w, source_h);
493
494 auto& ctx = reduce_context();
495 use_shader(ctx, "region_sample_batch.comp.spv", k_wg_reduce, sizeof(BatchCountPC));
496 ctx.stage_image(source);
497 ctx.set_binding_data(3, std::span<const uint32_t>(rect));
498 ctx.set_push_constants(BatchCountPC { .count = 1 });
499
500 const auto raw = dispatch_reduce(ctx, 256, 3 * sizeof(glm::vec4));
501 return raw.size() >= 12 ? field_sample(raw.data()) : Kinesis::Vision::FieldSample {};
502}
503
504std::vector<Kinesis::Vision::FieldSample> VisionExtractor::sample_regions(
505 const std::shared_ptr<Core::VKImage>& source,
506 const Kinesis::Vision::VisionAnalysis& analysis,
507 uint32_t source_w, uint32_t source_h)
508{
509 if (!analysis.find_elements || analysis.find_elements->boxes.empty())
510 return {};
511
512 const auto& boxes = analysis.find_elements->boxes;
513 const auto count = static_cast<uint32_t>(boxes.size());
514
515 std::vector<uint32_t> rects;
516 rects.reserve(boxes.size() * 4);
517 for (const auto& b : boxes) {
518 const auto rect = pixel_rect(b, source_w, source_h);
519 rects.insert(rects.end(), rect.begin(), rect.end());
520 }
521
522 auto& ctx = reduce_context();
523 use_shader(ctx, "region_sample_batch.comp.spv", k_wg_reduce, sizeof(BatchCountPC));
524 ctx.stage_image(source);
525 ctx.set_binding_data(3, std::span<const uint32_t>(rects));
526 ctx.set_push_constants(BatchCountPC { .count = count });
527
528 const auto raw = dispatch_reduce(ctx, 256U * count, static_cast<size_t>(count) * 3 * sizeof(glm::vec4));
529 std::vector<Kinesis::Vision::FieldSample> out;
530 if (raw.size() < static_cast<size_t>(count) * 12)
531 return out;
532
533 out.reserve(count);
534 for (uint32_t i = 0; i < count; ++i)
535 out.push_back(field_sample(raw.data() + static_cast<size_t>(i) * 12));
536 return out;
537}
538
539std::vector<glm::vec4> VisionExtractor::sample_points(
540 const std::shared_ptr<Core::VKImage>& source,
541 const Kinesis::Vision::VisionAnalysis& analysis,
542 uint32_t radius,
543 uint32_t source_w, uint32_t source_h)
544{
545 const auto points = points_for_analysis(analysis);
546 if (points.empty())
547 return {};
548
549 const auto count = static_cast<uint32_t>(points.size());
550
551 auto& ctx = reduce_context();
552 use_shader(ctx, "point_sample.comp.spv", k_wg_reduce, sizeof(PointSamplePC));
553 ctx.stage_image(source);
554 ctx.set_binding_data(3, std::span<const float>(
555 reinterpret_cast<const float*>(points.data()), points.size() * 2));
556 ctx.set_push_constants(PointSamplePC {
557 .count = count, .radius = radius, .src_w = source_w, .src_h = source_h });
558
559 const auto raw = dispatch_reduce(ctx, count, static_cast<size_t>(count) * sizeof(glm::vec4));
560 std::vector<glm::vec4> out;
561 if (raw.size() < static_cast<size_t>(count) * 4)
562 return out;
563
564 out.resize(count);
565 std::memcpy(out.data(), raw.data(), static_cast<size_t>(count) * sizeof(glm::vec4));
566 return out;
567}
568
569std::shared_ptr<Core::VKImage> VisionExtractor::patches(
570 const std::shared_ptr<Core::VKImage>& source,
571 const Kinesis::Vision::VisionAnalysis& analysis,
572 uint32_t patch_w, uint32_t patch_h,
573 uint32_t source_w, uint32_t source_h)
574{
575 const auto centers = points_for_analysis(analysis);
576 if (centers.empty())
577 return nullptr;
578
579 const auto count = static_cast<uint32_t>(centers.size());
580
581 auto& ctx = image_context();
582 use_shader(ctx, "vision_patch_extract.comp.spv", k_wg_image, sizeof(PatchExtractPC));
583 ctx.stage_image(source);
584 ctx.set_binding_data(3, std::span<const float>(
585 reinterpret_cast<const float*>(centers.data()), centers.size() * 2));
586 ctx.set_push_constants(PatchExtractPC {
587 .patch_w = patch_w, .patch_h = patch_h, .count = count,
588 .src_w = source_w, .src_h = source_h });
589 return dispatch_image(ctx, patch_w * count, patch_h);
590}
591
592std::shared_ptr<Core::VKImage> VisionExtractor::tone(
593 const std::shared_ptr<Core::VKImage>& source,
595 uint32_t mode, float gain,
596 uint32_t source_w, uint32_t source_h)
597{
598 auto& ctx = image_context();
599 use_shader(ctx, "histogram_equalize.comp.spv", k_wg_image, sizeof(TonePC));
600 ctx.stage_image(source);
601 ctx.set_binding_data(3, std::span<const uint32_t>(appearance.histogram.data(), appearance.histogram.size()));
602 ctx.set_push_constants(TonePC { .mode = mode, .gain = gain, .width = source_w, .height = source_h });
603 return dispatch_image(ctx, source_w, source_h);
604}
605
606std::shared_ptr<Core::VKImage> VisionExtractor::equalize(
607 const std::shared_ptr<Core::VKImage>& source,
608 const Kinesis::Vision::VisionAnalysis& analysis,
609 uint32_t source_w, uint32_t source_h)
610{
611 if (!analysis.measure_appearance)
612 return nullptr;
613
614 return tone(source, *analysis.measure_appearance, 0U, 1.0F, source_w, source_h);
615}
616
617std::shared_ptr<Core::VKImage> VisionExtractor::expose(
618 const std::shared_ptr<Core::VKImage>& source,
619 const Kinesis::Vision::VisionAnalysis& analysis,
620 float target,
621 uint32_t source_w, uint32_t source_h)
622{
623 if (!analysis.measure_appearance)
624 return nullptr;
625
626 const float mean = analysis.measure_appearance->mean_brightness;
627 const float gain = mean > 1e-4F ? target / mean : 1.0F;
628 return tone(source, *analysis.measure_appearance, 1U, gain, source_w, source_h);
629}
630
631std::shared_ptr<Core::VKImage> VisionExtractor::edges(
632 const std::shared_ptr<Core::VKImage>& source,
633 const Kinesis::Vision::VisionAnalysis& analysis,
634 uint32_t source_w, uint32_t source_h)
635{
636 if (!analysis.detect_edges)
637 return nullptr;
638
639 auto& ctx = image_context();
640 use_shader(ctx, "edge_select.comp.spv", k_wg_image, sizeof(EdgePC));
641 ctx.stage_image(source);
642 ctx.stage_image_at(2, analysis.detect_edges, ElementType::IMAGE_SAMPLED, default_sampler());
643 ctx.set_push_constants(EdgePC { .threshold = m_edge_threshold, .out_w = source_w, .out_h = source_h });
644 return dispatch_image(ctx, source_w, source_h);
645}
646
647std::shared_ptr<Core::VKImage> VisionExtractor::motion_mask(
648 const std::shared_ptr<Core::VKImage>& source,
649 const Kinesis::Vision::VisionAnalysis& analysis,
650 uint32_t source_w, uint32_t source_h)
651{
652 if (!analysis.estimate_motion)
653 return nullptr;
654 if (analysis.motion_activity && *analysis.motion_activity < m_min_activity)
655 return nullptr;
656
657 auto& ctx = image_context();
658 use_shader(ctx, "flow_mask.comp.spv", k_wg_image, sizeof(MotionPC));
659 ctx.stage_image(source);
660 ctx.stage_image_at(2, analysis.estimate_motion, ElementType::IMAGE_SAMPLED, default_sampler());
661 ctx.set_push_constants(MotionPC {
662 .min_speed = m_min_speed, .min_confidence = m_min_confidence,
663 .out_w = source_w, .out_h = source_h });
664 return dispatch_image(ctx, source_w, source_h);
665}
666
667std::shared_ptr<Core::VKImage> VisionExtractor::annotate(
668 const std::shared_ptr<Core::VKImage>& source,
669 const Kinesis::Vision::VisionAnalysis& analysis,
670 uint32_t source_w, uint32_t source_h)
671{
672 const glm::vec4 yellow { 1.0F, 1.0F, 0.0F, 1.0F };
673 const glm::vec4 cyan { 0.0F, 1.0F, 1.0F, 1.0F };
674 const glm::vec4 green { 0.0F, 1.0F, 0.0F, 1.0F };
675 const glm::vec4 red { 1.0F, 0.0F, 0.0F, 1.0F };
676 const glm::vec4 white { 1.0F, 1.0F, 1.0F, 1.0F };
677 const glm::vec4 magenta { 1.0F, 0.0F, 1.0F, 1.0F };
678
679 const glm::vec2 half { 3.0F / static_cast<float>(source_w), 3.0F / static_cast<float>(source_h) };
680
681 std::vector<float> segments;
682
683 if (analysis.find_elements) {
684 for (const auto& box : analysis.find_elements->boxes)
685 add_rect(segments, box, yellow);
686 for (const auto& contour : analysis.find_elements->contours) {
687 const auto n = contour.points.size();
688 for (size_t i = 0; n > 1 && i < n; ++i)
689 add_line(segments, contour.points[i], contour.points[(i + 1) % n], cyan);
690 }
691 }
692
693 if (analysis.track_objects && !analysis.track_objects->tracks.empty()) {
694 for (const auto& t : analysis.track_objects->tracks) {
695 add_line(segments, t.previous, t.position, t.tracked ? green : red);
696 add_cross(segments, t.position, half, t.tracked ? green : red);
697 }
698 add_rect(segments, analysis.track_objects->bounds, white);
699 add_cross(segments, analysis.track_objects->centroid, half, white);
700 }
701
702 if (analysis.detect_features && !analysis.detect_features->keypoints.empty()) {
703 for (const auto& k : analysis.detect_features->keypoints)
704 add_cross(segments, k.position, half, magenta);
705 add_rect(segments, analysis.detect_features->bounds, white);
706 }
707
708 const auto count = static_cast<uint32_t>(segments.size() / 8);
709 if (segments.empty())
710 segments.assign(8, 0.0F);
711
712 auto& ctx = image_context();
713 use_shader(ctx, "vision_crop.comp.spv", k_wg_image, sizeof(CropPC));
714 ctx.stage_image(source);
715 ctx.set_binding_data(3, std::span<const float>(segments));
716 ctx.set_push_constants(CropPC {
717 .src_x = 0.0F, .src_y = 0.0F, .src_w = 1.0F, .src_h = 1.0F,
718 .out_w = source_w, .out_h = source_h });
719 auto image = dispatch_image(ctx, source_w, source_h);
720
721 if (count == 0)
722 return image;
723
724 ctx.swap_shader({
725 .shader_path = "overlay_segments.comp.spv",
726 .workgroup_size = k_wg_image,
727 .push_constant_size = sizeof(SegmentsPC),
728 });
729 ctx.set_push_constants(SegmentsPC { .count = count, .width = source_w, .height = source_h });
730 ctx.set_output_dimensions(source_w, source_h);
731 dispatch_and_wait(ctx);
732
733 return ctx.get_output_image(0);
734}
735
736std::shared_ptr<Core::VKImage> VisionExtractor::resolve_image(
737 const std::shared_ptr<Kakshya::SignalSourceContainer>& source,
738 size_t index)
739{
740 if (!source)
741 return nullptr;
742
743 if (auto tc = std::dynamic_pointer_cast<Kakshya::TextureContainer>(source))
744 return tc->to_image(static_cast<uint32_t>(index));
745
746 if (auto wc = std::dynamic_pointer_cast<Kakshya::WindowContainer>(source))
747 return wc->to_image();
748
749 auto vc = std::dynamic_pointer_cast<Kakshya::VideoStreamContainer>(source);
750 if (!vc)
751 return nullptr;
752
753 const uint32_t w = vc->get_width();
754 const uint32_t h = vc->get_height();
755
756 const void* raw = source->get_raw_data();
757 if (!raw)
758 return nullptr;
759
760 if (!m_upload_image || m_upload_w != w || m_upload_h != h) {
762 w, h, Portal::Graphics::ImageFormat::RGBA8, nullptr);
763 m_upload_w = w;
764 m_upload_h = h;
765 }
766 if (!m_upload_image)
767 return nullptr;
768
770 return m_upload_image;
771}
772
774{
775 output_type output;
776 output.metadata = input.metadata;
777
778 const auto supplied = Kakshya::get_metadata_value<std::shared_ptr<Core::VKImage>>(input.metadata, "vision_image");
779 const bool has_image = supplied && *supplied;
780
781 if (!has_image && !input.data) {
782 output.metadata["error"] = std::string("VisionExtractor: missing container");
783 return output;
784 }
785
786 const auto analysis = Kakshya::get_metadata_value<Kinesis::Vision::VisionAnalysis>(
787 input.metadata, "vision_analysis");
788 if (!analysis) {
789 output.metadata["error"] = std::string("VisionExtractor: missing vision_analysis metadata");
790 return output;
791 }
792
793 const auto index = Kakshya::get_metadata_value<size_t>(input.metadata, "container_index").value_or(0);
794 const auto image = has_image ? *supplied : resolve_image(input.data, index);
795 if (!image) {
796 output.metadata["error"] = std::string("VisionExtractor: could not resolve image from container");
797 return output;
798 }
799
800 const uint32_t w = image->get_width();
801 const uint32_t h = image->get_height();
802
803 switch (m_mode) {
805 output.metadata["vision_extraction"] = crop(image, *analysis, m_out_w, m_out_h);
806 break;
808 output.metadata["vision_extraction"] = sample(image, *analysis, w, h);
809 break;
811 output.metadata["vision_extraction"] = patches(image, *analysis, m_patch_w, m_patch_h, w, h);
812 break;
814 output.metadata["vision_extraction"] = crops(image, *analysis, m_tile_w, m_tile_h);
815 break;
817 output.metadata["vision_extraction"] = sample_regions(image, *analysis, w, h);
818 break;
820 output.metadata["vision_extraction"] = sample_points(image, *analysis, m_sample_radius, w, h);
821 break;
823 output.metadata["vision_extraction"] = silhouettes(image, *analysis, m_tile_w, m_tile_h);
824 break;
826 output.metadata["vision_extraction"] = equalize(image, *analysis, w, h);
827 break;
829 output.metadata["vision_extraction"] = expose(image, *analysis, m_exposure_target, w, h);
830 break;
832 output.metadata["vision_extraction"] = edges(image, *analysis, w, h);
833 break;
835 output.metadata["vision_extraction"] = motion_mask(image, *analysis, w, h);
836 break;
838 output.metadata["vision_extraction"] = annotate(image, *analysis, w, h);
839 break;
841 if (const auto selected = select_element(*analysis, m_focus.value_or(ElementFocus {})))
842 output.metadata["vision_extraction"] = *selected;
843 break;
844 }
845
846 return output;
847}
848
849} // namespace MayaFlux::Yantra
vk::Fence fence
Core::GlobalInputConfig input
Definition Config.cpp:42
uint32_t label
Definition Contours.cpp:16
glm::vec3 color
Definition Decoder.cpp:97
IO::ImageData image
Definition Decoder.cpp:47
std::vector< uint32_t > indices
std::vector< glm::vec2 > * points
float radius
uint32_t h
Definition InkPress.cpp:29
uint32_t mode
size_t a
size_t b
std::vector< float > * out
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
uint32_t index
Definition VKDevice.cpp:122
size_t count
uint32_t py
uint32_t px
uint32_t criterion
uint32_t out_h
float src_w
float threshold
float min_confidence
uint32_t largest
uint32_t tile_h
uint32_t patch_w
float src_h
float src_y
uint32_t tile_w
uint32_t patch_h
float src_x
float gain
uint32_t out_w
float min_speed
GPU getter for what VisionAnalyzer found.
uint32_t width
uint32_t height
vk::Sampler get_default_linear()
Get a default linear sampler.
void upload_data(const std::shared_ptr< Core::VKImage > &image, const void *data, size_t size)
Upload pixel data to an existing texture.
std::shared_ptr< Core::VKImage > create_2d(uint32_t width, uint32_t height, ImageFormat format=ImageFormat::RGBA8, const void *data=nullptr, uint32_t mip_levels=1)
Create a 2D texture.
GpuExecutionContext specialisation for image compute shaders.
std::shared_ptr< Core::VKImage > edges(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t source_w, uint32_t source_h)
source kept where analysis.detect_edges reaches the edge threshold, transparent elsewhere.
std::shared_ptr< Core::VKImage > tone(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::MeasureAppearanceAnalysis &appearance, uint32_t mode, float gain, uint32_t source_w, uint32_t source_h)
std::vector< glm::vec4 > sample_points(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t radius, uint32_t source_w, uint32_t source_h)
Mean over a (2 * radius + 1) squared window at every keypoint, else every track, in one dispatch,...
std::shared_ptr< Core::VKImage > m_upload_image
std::vector< glm::vec2 > points_for_analysis(const Kinesis::Vision::VisionAnalysis &analysis) const
std::shared_ptr< TextureExecutionContext > m_reduce_ctx
std::shared_ptr< Core::VKImage > resolve_image(const std::shared_ptr< Kakshya::SignalSourceContainer > &source, size_t index)
output_type run_operation(const input_type &input)
std::shared_ptr< Core::VKImage > crop(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t out_w, uint32_t out_h)
Crop of source at the focused element, or track/keypoint bounds.
std::shared_ptr< Core::VKImage > equalize(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t source_w, uint32_t source_h)
Histogram equalisation of source from the histogram measure_appearance measured.
std::optional< uint32_t > label_for_analysis(const Kinesis::Vision::VisionAnalysis &analysis)
Kinesis::Vision::FieldSample sample(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t source_w, uint32_t source_h)
Mean, min and max over the focused element or track/keypoint bounds.
std::shared_ptr< Core::VKImage > silhouette(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t tile_w, uint32_t tile_h)
silhouettes() for the focused element only.
TextureExecutionContext & reduce_context()
std::shared_ptr< Core::VKImage > patches(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t patch_w, uint32_t patch_h, uint32_t source_w, uint32_t source_h)
One fixed-size crop per keypoint, else per track, side by side in one atlas.
std::optional< Kinesis::Vision::BoundingBox > region_for_analysis(const Kinesis::Vision::VisionAnalysis &analysis)
std::shared_ptr< Core::VKImage > crops(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t tile_w, uint32_t tile_h)
Every find_elements box resampled to tile_w x tile_h, side by side in one atlas (tile_w * count,...
std::shared_ptr< Core::VKImage > expose(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, float target, uint32_t source_w, uint32_t source_h)
source scaled so its measured mean brightness reaches target.
std::shared_ptr< TextureExecutionContext > m_image_ctx
std::optional< ElementFocus > m_focus
std::shared_ptr< Core::VKImage > annotate(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t source_w, uint32_t source_h)
source with everything the analysis found drawn on it: element boxes (yellow) and contours (cyan),...
std::vector< Kinesis::Vision::FieldSample > sample_regions(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t source_w, uint32_t source_h)
sample() for every find_elements box in one dispatch, in box order.
std::optional< size_t > focus_box_index(const Kinesis::Vision::VisionAnalysis &analysis)
Box index of the element the single-element methods act on.
std::shared_ptr< Core::VKImage > mask(VisionGpuExecutor &executor, const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t w, uint32_t h)
Pixel-precise silhouette of the focused element from the executor's label buffer.
std::optional< size_t > select_element(const Kinesis::Vision::VisionAnalysis &analysis, const ElementFocus &focus)
Index into find_elements boxes of the element ranked first by focus, chosen on the GPU from the analy...
std::shared_ptr< Core::VKImage > silhouettes(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t tile_w, uint32_t tile_h)
Silhouette of every element cut from its contour polygons: the outer contour and its holes,...
std::shared_ptr< Core::VKImage > fill_elements(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::FindElementsAnalysis &elements, const std::vector< size_t > &indices, uint32_t tile_w, uint32_t tile_h)
std::shared_ptr< Core::VKImage > motion_mask(const std::shared_ptr< Core::VKImage > &source, const Kinesis::Vision::VisionAnalysis &analysis, uint32_t source_w, uint32_t source_h)
source kept where analysis.estimate_motion moves faster than the motion limits with enough confidence...
TextureExecutionContext & image_context()
The two contexts every method shares, one shader swapped in per call.
VisionExtractor(VisionExtractMode mode=VisionExtractMode::Crop)
std::shared_ptr< Core::VKImage > select_label(VisionGpuContexts &contexts, const std::shared_ptr< Core::VKImage > &source, uint32_t target_label, uint32_t w, uint32_t h)
Isolate one ConnectedComponents label's silhouette on GPU, through the explicit context set that prod...
Stateful GPU dispatch engine for VisionSequence execution.
OutputMode
Defines how the network's computational results are exposed.
MAYAFLUX_API ShaderFoundry & get_shader_foundry()
Get the global shader compiler instance.
Portal::Graphics::GpuBufferBinding GpuBufferBinding
VisionExtractMode
Which method run_operation dispatches to.
Per-channel mean/min/max over a sampled image region, computed on GPU (Yantra's region_sample....
FindElements output: contours plus the per-component BoundingBoxes ConnectedComponents already comput...
MeasureAppearance output: whole-frame photometric characterisation, computed on GPU (region_sample....
std::optional< FindElementsAnalysis > find_elements
std::optional< MeasureAppearanceAnalysis > measure_appearance
std::optional< DetectFeaturesAnalysis > detect_features
std::shared_ptr< Core::VKImage > estimate_motion
std::optional< TrackObjectsAnalysis > track_objects
std::shared_ptr< Core::VKImage > detect_edges
std::optional< float > motion_activity
VisionResult::motion_energy from an active EstimateMotion sequence.
Composed analysis output: one populated field per active intent that produced a result this call.
ElementType
Element type the shader expects in this binding.
Direction
Data flow direction for this binding.
std::unordered_map< std::string, std::any > metadata
Associated metadata.
Definition DataIO.hpp:30
Input/Output container for computation pipeline data flow with structure preservation.
Definition DataIO.hpp:26
How the extractor chooses among several found elements: rank by criterion, highest first when largest...