MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
VisionImageDispatch.cpp
Go to the documentation of this file.
1#include "DispatchLayouts.hpp"
2
3namespace MayaFlux::Yantra {
4
5using namespace VisionInternal;
6
7using namespace Portal::Graphics;
8using namespace Kinesis::Vision;
9
10const std::vector<float>& VisionGpuExecutor::gaussian_kernel_2d(uint32_t radius, float sigma)
11{
12 static std::unordered_map<uint64_t, std::vector<float>> cache;
13 const uint64_t key = (static_cast<uint64_t>(std::bit_cast<uint32_t>(sigma)) << 32)
14 | radius;
15 auto it = cache.find(key);
16 if (it != cache.end())
17 return it->second;
18
19 const uint32_t diam = 2 * radius + 1;
20 std::vector<float> k(static_cast<size_t>(diam) * diam);
21 float sum = 0.0F;
22 for (uint32_t y = 0; y < diam; ++y) {
23 for (uint32_t x = 0; x < diam; ++x) {
24 const float fx = static_cast<float>(x) - static_cast<float>(radius);
25 const float fy = static_cast<float>(y) - static_cast<float>(radius);
26 const float v = std::exp(-(fx * fx + fy * fy) / (2.0F * sigma * sigma));
27 k[y * diam + x] = v;
28 sum += v;
29 }
30 }
31 for (auto& v : k)
32 v /= sum;
33
34 return cache.emplace(key, std::move(k)).first->second;
35}
36
37GpuVisionPass::Completed VisionGpuExecutor::op_threshold_otsu(VisionGpuContexts& contexts, const OtsuParams& p)
38{
39 auto& pixel_ctx = contexts.pixel;
40 auto w = contexts.pass.w;
41 auto h = contexts.pass.h;
43
44 const auto otsu_input = contexts.pass.current;
45
46 pixel_ctx.ensure_shared_buffer(0, 3, 768, GpuBufferBinding::ElementType::UINT32,
48 pixel_ctx.ensure_shared_buffer(0, 4, 3, GpuBufferBinding::ElementType::UINT32,
50
51 const std::array<uint32_t, 768> histogram_reset {};
52 pixel_ctx.upload_shared_raw(0, 3, reinterpret_cast<const uint8_t*>(histogram_reset.data()), sizeof(histogram_reset));
53
54 const auto channels = static_cast<uint32_t>(p.channels);
55 const OtsuHistPC hist_pc { .width = w, .height = h, .channels = channels };
56 const OtsuApplyPC apply_pc { .channels = channels };
57 const uint32_t select_groups = channels == 0U ? 1U : 3U;
58 const auto otsu_hazard = [](uint32_t binding) {
59 return [binding](GpuDispatchCore& ctx) {
60 return std::vector<HazardResource> {
61 ctx.shared_buffer_hazard({ .set = 0, .binding = binding, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::UINT32 }),
62 };
63 };
64 };
65 const std::array<uint32_t, 3> image_groups { (w + k_wg2d[0] - 1U) / k_wg2d[0], (h + k_wg2d[1] - 1U) / k_wg2d[1], 1U };
66
67 std::shared_ptr<Core::VKImage> thresholded;
68 std::vector<DependencyStage> otsu_stages;
69 otsu_stages.reserve(3);
70 otsu_stages.push_back({
71 .config = { .shader_path = "otsu_histogram.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(OtsuHistPC) },
72 .stage_fn = [&pixel_ctx, otsu_input, hist_pc](GpuDispatchCore& ctx) {
73 pixel_ctx.stage_image(otsu_input);
74 ctx.set_push_constants(hist_pc); },
75 .hazard_fn = otsu_hazard(3),
76 .explicit_groups = image_groups,
77 });
78 otsu_stages.push_back({
79 .config = { .shader_path = "otsu_select.comp.spv", .workgroup_size = { 256, 1, 1 } },
80 .stage_fn = [&pixel_ctx, otsu_input](GpuDispatchCore&) { pixel_ctx.stage_image(otsu_input); },
81 .hazard_fn = otsu_hazard(4),
82 .explicit_groups = std::array<uint32_t, 3> { select_groups, 1U, 1U },
83 });
84 otsu_stages.push_back({
85 .config = { .shader_path = "otsu_apply.comp.spv", .workgroup_size = k_wg2d, .push_constant_size = sizeof(OtsuApplyPC) },
86 .stage_fn = [&pixel_ctx, &thresholded, otsu_input, w, h, apply_pc](GpuDispatchCore& ctx) {
87 pixel_ctx.stage_image(otsu_input);
88 ctx.set_push_constants(apply_pc);
89 pixel_ctx.prepare_output_image(w, h);
90 thresholded = pixel_ctx.get_output_image(0); },
91 .explicit_groups = image_groups,
92 });
93
94 {
95 ExecutionContext otsu_ctx;
97 DependencyParams otsu_params;
98 otsu_params.stages = otsu_stages;
99 otsu_params.async = true;
100 otsu_ctx.parameters = otsu_params;
101 const auto result = pixel_ctx.execute(Datum<> {}, otsu_ctx);
102 const auto f = result.get_metadata<FenceID>("gpu_fence").value_or(INVALID_FENCE);
103 foundry.wait_for_fence(f);
104 foundry.release_fence(f);
105 }
106
107 contexts.pass.result.images.threshold_otsu = thresholded;
108 contexts.pass.current = thresholded;
109 contexts.pass.result.structured = std::monostate {};
110
111 return { .output = thresholded, .input = otsu_input };
112}
113
114GpuVisionPass::Completed VisionGpuExecutor::op_confine(
115 VisionGpuContexts& contexts,
116 const ConfineParams& p)
117{
118 auto& pixel_ctx = contexts.pixel;
119 auto w = contexts.pass.w;
120 auto h = contexts.pass.h;
121 auto& foundry = Portal::Graphics::get_shader_foundry();
122
123 const auto confine_input = contexts.pass.current;
124
125 const auto src_x = static_cast<uint32_t>(p.bounds.x * static_cast<float>(w));
126 const auto src_y = static_cast<uint32_t>(p.bounds.y * static_cast<float>(h));
127 const auto src_w = std::max<uint32_t>(1U, static_cast<uint32_t>(p.bounds.w * static_cast<float>(w)));
128 const auto src_h = std::max<uint32_t>(1U, static_cast<uint32_t>(p.bounds.h * static_cast<float>(h)));
129
130 const uint32_t out_w = p.resize_to_source ? w : src_w;
131 const uint32_t out_h = p.resize_to_source ? h : src_h;
132
133 pixel_ctx.swap_shader({
134 .shader_path = "vision_confine.comp.spv",
135 .workgroup_size = k_wg2d,
136 .push_constant_size = sizeof(ConfinePC),
137 });
138 pixel_ctx.stage_image(confine_input);
139 pixel_ctx.set_push_constants(ConfinePC {
140 .src_x = src_x, .src_y = src_y, .src_w = src_w, .src_h = src_h,
141 .out_w = out_w, .out_h = out_h });
142 pixel_ctx.prepare_output_image(out_w, out_h);
143 pixel_ctx.set_output_dimensions(out_w, out_h);
144
145 const auto fence = pixel_ctx.dispatch_async({});
146 pixel_ctx.clear_output_dimensions();
147 foundry.wait_for_fence(fence);
148 foundry.release_fence(fence);
149
150 auto confined = pixel_ctx.get_output_image(0);
151
152 contexts.pass.result.images.confine = confined;
153 contexts.pass.current = confined;
154 contexts.pass.result.structured = std::monostate {};
155 contexts.bound_staged.reset();
156
157 contexts.pass.set_geometry(out_w, out_h);
158 contexts.pass.storage_w = out_w;
159 contexts.pass.storage_h = out_h;
160
161 return { .output = confined, .input = confine_input };
162}
163
164GpuVisionPass::Completed VisionGpuExecutor::op_open_close(
165 VisionGpuContexts& contexts,
166 VisionOp op,
167 const MorphParams& p)
168{
169 auto& pixel_ctx = contexts.pixel;
170 auto w = contexts.pass.w;
171 auto h = contexts.pass.h;
172 auto& foundry = Portal::Graphics::get_shader_foundry();
173
174 const auto morph_input = contexts.pass.current;
175 const auto radius = p.radius;
176 const bool is_open = (op == VisionOp::Open);
177 const MorphPC morph_pc { .radius = radius };
178
179 const GpuComputeConfig first_cfg {
180 .shader_path = is_open ? "erode.comp.spv" : "dilate.comp.spv",
181 .workgroup_size = k_wg2d,
182 .push_constant_size = sizeof(MorphPC),
183 };
184 const GpuComputeConfig second_cfg {
185 .shader_path = is_open ? "dilate.comp.spv" : "erode.comp.spv",
186 .workgroup_size = k_wg2d,
187 .push_constant_size = sizeof(MorphPC),
188 };
189 const std::array<uint32_t, 3> image_groups { (w + k_wg2d[0] - 1U) / k_wg2d[0], (h + k_wg2d[1] - 1U) / k_wg2d[1], 1U };
190
191 std::shared_ptr<Core::VKImage> intermediate;
192 std::shared_ptr<Core::VKImage> opened_closed;
193
194 std::vector<DependencyStage> morph_stages;
195 morph_stages.reserve(2);
196 morph_stages.push_back({
197 .config = first_cfg,
198 .stage_fn = [&pixel_ctx, &intermediate, morph_input, morph_pc, w, h](GpuDispatchCore& ctx) {
199 pixel_ctx.stage_image(morph_input);
200 pixel_ctx.prepare_output_image(w, h);
201 intermediate = pixel_ctx.get_output_image(0);
202 ctx.set_push_constants(morph_pc); },
203 .hazard_fn = [&intermediate](GpuDispatchCore&) { return std::vector<HazardResource> {
204 { .binding = { .set = 0, .binding = 1, .direction = GpuBufferBinding::Direction::INPUT_OUTPUT, .element_type = GpuBufferBinding::ElementType::IMAGE_STORAGE },
205 .image = intermediate->get_image() },
206 }; },
207 .explicit_groups = image_groups,
208 });
209 morph_stages.push_back({
210 .config = second_cfg,
211 .stage_fn = [&pixel_ctx, &intermediate, &opened_closed, morph_pc, w, h](GpuDispatchCore& ctx) {
212 pixel_ctx.stage_image(intermediate);
213 pixel_ctx.prepare_output_image(w, h);
214 opened_closed = pixel_ctx.get_output_image(0);
215 ctx.set_push_constants(morph_pc); },
216 .explicit_groups = image_groups,
217 });
218
219 {
220 ExecutionContext morph_ctx;
222 DependencyParams morph_params;
223 morph_params.stages = morph_stages;
224 morph_params.async = true;
225 morph_ctx.parameters = morph_params;
226 const auto result = pixel_ctx.execute(Datum<> {}, morph_ctx);
227 const auto f = result.get_metadata<FenceID>("gpu_fence").value_or(INVALID_FENCE);
228 foundry.wait_for_fence(f);
229 foundry.release_fence(f);
230 }
231
232 if (is_open) {
233 contexts.pass.result.images.open = opened_closed;
234 } else {
235 contexts.pass.result.images.close = opened_closed;
236 }
237 contexts.pass.current = opened_closed;
238 contexts.pass.result.structured = std::monostate {};
239
240 return { .output = opened_closed, .input = morph_input };
241}
242
243GpuVisionPass::Completed VisionGpuExecutor::op_canny(
244 VisionGpuContexts& contexts,
245 const VisionParams& params,
246 const CannyParams& p)
247{
248 auto& pixel_ctx = contexts.pixel;
249 auto& label_ctx = contexts.labels;
250 auto w = contexts.pass.w;
251 auto h = contexts.pass.h;
252 auto& foundry = Portal::Graphics::get_shader_foundry();
253
254 const auto canny_input = contexts.pass.current;
255
256 const auto radius = static_cast<uint32_t>(std::ceil(p.sigma * 3.0F));
257 const auto& weights = gaussian_kernel_2d(radius, p.sigma);
258 const auto blur_cfg = VisionGpuExecutor::config(VisionOp::GaussianBlur, GaussianBlurParams { .sigma = p.sigma });
259 pixel_ctx.swap_shader(blur_cfg);
260 pixel_ctx.stage_image(canny_input);
261 pixel_ctx.set_binding_data(2, std::span<const float>(weights));
262 pixel_ctx.set_push_constants(GaussianPC { .radius = radius, .width = w, .height = h });
263 pixel_ctx.prepare_output_image(w, h);
264 {
265 const auto f = pixel_ctx.dispatch_async({});
266 foundry.wait_for_fence(f);
267 foundry.release_fence(f);
268 }
269 const auto blurred = pixel_ctx.get_output_image(0);
270
271 const auto sobel_cfg = VisionGpuExecutor::config(VisionOp::Sobel, std::monostate {});
272 pixel_ctx.swap_shader(sobel_cfg);
273 pixel_ctx.stage_image(blurred);
274 pixel_ctx.prepare_output_image(w, h);
275 {
276 const auto f = pixel_ctx.dispatch_async({});
277 foundry.wait_for_fence(f);
278 foundry.release_fence(f);
279 }
280 const auto grad = pixel_ctx.get_output_image(0);
281
282 const GpuComputeConfig nms_cfg {
283 .shader_path = "canny_nms.comp.spv",
284 .workgroup_size = k_wg2d,
285 };
286 pixel_ctx.swap_shader(nms_cfg);
287 pixel_ctx.stage_image(grad);
288 pixel_ctx.prepare_output_image(w, h);
289 {
290 const auto f = pixel_ctx.dispatch_async({});
291 foundry.wait_for_fence(f);
292 foundry.release_fence(f);
293 }
294 auto suppressed = pixel_ctx.get_output_image(0);
295
296 const auto classify_cfg = VisionGpuExecutor::config(VisionOp::Canny, params);
297 pixel_ctx.swap_shader(classify_cfg);
298 pixel_ctx.stage_image(suppressed);
299 pixel_ctx.set_push_constants(ClassifyPC { .threshold = p.low_threshold, .value = 0.5F });
300 pixel_ctx.prepare_output_image(w, h);
301 {
302 const auto f = pixel_ctx.dispatch_async({});
303 foundry.wait_for_fence(f);
304 foundry.release_fence(f);
305 }
306 auto classified_weak = pixel_ctx.get_output_image(0);
307
308 pixel_ctx.stage_image(classified_weak);
309 pixel_ctx.set_push_constants(ClassifyPC { .threshold = p.high_threshold, .value = 1.0F });
310 pixel_ctx.prepare_output_image(w, h);
311 {
312 const auto f = pixel_ctx.dispatch_async({});
313 foundry.wait_for_fence(f);
314 foundry.release_fence(f);
315 }
316 auto classified = pixel_ctx.get_output_image(0);
317
318 constexpr uint32_t k_max_hysteresis_rounds = 64;
319 label_ctx.set_output_size(2, sizeof(uint32_t));
320 label_ctx.set_output_dimensions(w, h);
321 label_ctx.swap_shader({
322 .shader_path = "canny_hysteresis.comp.spv",
323 .workgroup_size = k_wg2d,
324 .push_constant_size = sizeof(HysteresisPC),
325 });
326 label_ctx.stage_image_at(0, classified, GpuBufferBinding::ElementType::IMAGE_STORAGE);
327 label_ctx.slot_binding(0).direction = GpuBufferBinding::Direction::INPUT_OUTPUT;
328 {
329 uint32_t zero = 0;
330 label_ctx.set_binding_data(2, std::span<const uint32_t>(&zero, 1));
331 const HysteresisPC hpc { .width = w, .height = h };
332 ExecutionContext chained_ctx;
333 chained_ctx.mode = ExecutionMode::CHAINED;
334 chained_ctx.parameters = ChainedParams {
335 .pass_count = k_max_hysteresis_rounds,
336 .pc_updater = [hpc](uint32_t, void* dst) { std::memcpy(dst, &hpc, sizeof(HysteresisPC)); },
337 .passes_per_batch = k_max_hysteresis_rounds,
338 };
339 label_ctx.execute(Datum<> {}, chained_ctx);
340 }
341 label_ctx.slot_binding(0).direction = GpuBufferBinding::Direction::OUTPUT;
342 auto hysteresis_result = classified;
343
344 const auto finalize_cfg = config_from_spec(
346 .storage_image("out", BindingDirection::Output)
347 .storage_image("src", BindingDirection::Input)
348 .pc("threshold")
349 .op(KernelOp::CompareGE)
350 .workgroup(k_wg2d[0], k_wg2d[1])
351 .build());
352 pixel_ctx.swap_shader(finalize_cfg);
353 pixel_ctx.stage_image(hysteresis_result);
354 pixel_ctx.set_push_constants(FinalizePC { .threshold = 1.0F });
355 pixel_ctx.prepare_output_image(w, h);
356 {
357 const auto f = pixel_ctx.dispatch_async({});
358 foundry.wait_for_fence(f);
359 foundry.release_fence(f);
360 }
361 auto finalized = pixel_ctx.get_output_image(0);
362
363 contexts.pass.result.images.canny = finalized;
364 contexts.pass.current = finalized;
365 contexts.pass.result.structured = std::monostate {};
366
367 contexts.bound_config = finalize_cfg;
368 contexts.bound_staged = hysteresis_result;
369
370 return { .output = finalized, .input = canny_input };
371}
372
373} // namespace MayaFlux::Yantra
vk::Fence fence
float radius
uint32_t h
Definition InkPress.cpp:29
uint32_t * dst
uint32_t channels
uint32_t out_h
float src_w
float src_h
float src_y
float src_x
uint32_t out_w
Assemble & storage_image(std::string name, BindingDirection direction=BindingDirection::Output)
Declare a storage image binding (image2D).
Fluent assembler producing a ShaderSpec.
Non-template base that owns all type-independent GPU dispatch logic.
const std::vector< float > & gaussian_kernel_2d(uint32_t radius, float sigma)
2D Gaussian kernel for convolution, cached by (radius, sigma bit pattern).
Kinesis::Vision::GpuVisionPass::Completed op_canny(VisionGpuContexts &contexts, const Kinesis::Vision::VisionParams &params, const Kinesis::Vision::CannyParams &p)
Detect edges in the current image with the Canny pipeline.
static GpuComputeConfig config(Kinesis::Vision::VisionOp op, const Kinesis::Vision::VisionParams &params)
GpuComputeConfig for a given VisionOp and its parameters.
Kinesis::Vision::GpuVisionPass::Completed op_open_close(VisionGpuContexts &contexts, Kinesis::Vision::VisionOp op, const Kinesis::Vision::MorphParams &p)
Apply morphological opening or closing to the current image.
Kinesis::Vision::GpuVisionPass::Completed op_threshold_otsu(VisionGpuContexts &contexts, const Kinesis::Vision::OtsuParams &p)
Apply an Otsu threshold to the current image.
Kinesis::Vision::GpuVisionPass::Completed op_confine(VisionGpuContexts &contexts, const Kinesis::Vision::ConfineParams &p)
Arbitrary rectangular sub-region of the current image, optionally resized back to the frame's own siz...
VisionOp
Named operations available in a VisionSequence.
Definition VisionOp.hpp:30
std::variant< std::monostate, ThresholdParams, ThresholdAdaptiveParams, OtsuParams, NormalizeRangeParams, GaussianBlurParams, FilterSeparableParams, CannyParams, MorphParams, HarrisParams, ExtractPeaksParams, TrackKeypointsParams, ConnectedComponentsParams, FindContoursParams, OpticalFlowDenseParams, ConfineParams > VisionParams
Parameter variant covering all ops that carry parameters.
Definition VisionOp.hpp:358
constexpr FenceID INVALID_FENCE
MAYAFLUX_API ShaderFoundry & get_shader_foundry()
Get the global shader compiler instance.
@ COMPUTE
Storage buffer for compute shaders (device-local)
constexpr std::array< uint32_t, 3 > k_wg2d
Standard 2D workgroup used by all pixel-to-pixel vision shaders.
@ CHAINED
Part of a sequential chain.
@ DEPENDENCY
Part of dependency graph.
GpuComputeConfig config_from_spec(const Portal::Graphics::ShaderSpec &spec)
Derive a GpuComputeConfig from a ShaderSpec.
std::shared_ptr< Core::VKImage > close
std::shared_ptr< Core::VKImage > confine
std::shared_ptr< Core::VKImage > threshold_otsu
std::shared_ptr< Core::VKImage > open
std::shared_ptr< Core::VKImage > canny
void set_geometry(uint32_t width, uint32_t height) noexcept
ImageOutputs images
Image output of whichever image-producing ops ran in this sequence, one named slot per op.
Plain-data description of the compute shader to dispatch.
Parameters for ExecutionMode::CHAINED.
Input/Output container for computation pipeline data flow with structure preservation.
Definition DataIO.hpp:26
bool async
Submit the sequence without waiting for it to complete.
std::vector< DependencyStage > stages
Parameters for ExecutionMode::DEPENDENCY.
ExecutionMode mode
Execution mode controlling scheduling behavior.
ExecutionParams parameters
Optional parameters specific to the execution mode.
Context information controlling how a compute operation executes.
GpuComputeConfig bound_config
Shader currently bound on pixel, and the image staged into it.
TextureExecutionContext labels
Image + aux SSBO.
Kinesis::Vision::GpuVisionPass pass
Walk state for the current run: sequence position, geometry, working image, and the result under cons...
std::shared_ptr< Core::VKImage > bound_staged
TextureExecutionContext pixel
Image pipeline.
Fixed set of TextureExecutionContexts covering every GPU-implemented VisionOp shape.