ReUseX  0.0.5
3D Point Cloud Processing for Building Reuse
Loading...
Searching...
No Matches
labelLayoutSolver.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2025 Povl Filip Sonne-Frederiksen
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5#pragma once
6#include <algorithm>
7#include <cmath>
8#include <cstdint>
9#include <cstring>
10#include <functional>
11#include <limits>
12#include <random>
13#include <vector>
14
15namespace reusex::vision::osd {
16
19struct LayoutBox {
20 float left;
21 float top;
22 float right;
23 float bottom;
24
26 inline float width() const { return right - left; }
27
29 inline float height() const { return bottom - top; }
30
32 inline float area() const {
33 return std::max(0.0f, right - left) * std::max(0.0f, bottom - top);
34 }
35
37 static inline float intersectArea(const LayoutBox &box1,
38 const LayoutBox &box2) {
39 float l = std::max(box1.left, box2.left);
40 float r = std::min(box1.right, box2.right);
41 float t = std::max(box1.top, box2.top);
42 float b = std::min(box1.bottom, box2.bottom);
43 float w = std::max(0.0f, r - l);
44 float h = std::max(0.0f, b - t);
45 return w * h;
46 }
47
49 static inline bool intersects(const LayoutBox &box1, const LayoutBox &box2) {
50 return (box1.left < box2.right && box1.right > box2.left &&
51 box1.top < box2.bottom && box1.bottom > box2.top);
52 }
53};
54
56struct TextSize {
57 int width;
58 int height;
60};
61
64 float x;
65 float y;
67 int width;
68 int height;
70};
71
74 int gridSize = 100;
76 20;
78 30;
79 int paddingX = 2;
80 int paddingY = 2;
81
83 float costPos1_Top = 0.0f;
85 float costPos2_Right = 10.0f;
87 float costPos3_Bottom = 20.0f;
89 float costPos4_Left = 30.0f;
90
92 float costSlidingPenalty = 100.0f;
95 float costScaleTier = 10000.0f;
97 float costOccludeObj = 100000.0f;
99 float costOverlapBase = 100000.0f;
100};
101
108 public:
109 int rows = 0, cols = 0;
110 float cellW = 100.0f, cellH = 100.0f;
111 float invCellW = 0.01f, invCellH = 0.01f;
112
113 std::vector<int> gridHead;
114 struct Node {
115 int id;
116 int next;
117 };
118 std::vector<Node> nodes;
119
120 FlatUniformGrid() { nodes.reserve(4096); }
121
124 void resize(int w, int h, int gridSize) {
125 if (gridSize <= 0)
126 gridSize = 100;
127 int newCols = (w + gridSize - 1) / gridSize;
128 int newRows = (h + gridSize - 1) / gridSize;
129 cellW = (float)gridSize;
130 cellH = (float)gridSize;
131 invCellW = 1.0f / cellW;
132 invCellH = 1.0f / cellH;
133
134 if (newCols * newRows > (int)gridHead.size()) {
135 gridHead.resize(newCols * newRows, -1);
136 }
137 cols = newCols;
138 rows = newRows;
139 }
140
142 void clear() {
143 if (!gridHead.empty()) {
144 std::fill(gridHead.begin(), gridHead.begin() + (rows * cols), -1);
145 }
146 nodes.clear();
147 }
148
150 inline void insert(int id, const LayoutBox &box) {
151 int c1 = std::max(0, std::min(cols - 1, (int)(box.left * invCellW)));
152 int r1 = std::max(0, std::min(rows - 1, (int)(box.top * invCellH)));
153 int c2 = std::max(0, std::min(cols - 1, (int)(box.right * invCellW)));
154 int r2 = std::max(0, std::min(rows - 1, (int)(box.bottom * invCellH)));
155
156 for (int r = r1; r <= r2; ++r) {
157 int rowOffset = r * cols;
158 for (int c = c1; c <= c2; ++c) {
159 int idx = rowOffset + c;
160 nodes.push_back({id, gridHead[idx]});
161 gridHead[idx] = (int)nodes.size() - 1;
162 }
163 }
164 }
165
169 template <typename Visitor>
170 inline void query(const LayoutBox &box, std::vector<int> &visitedToken,
171 int cookie, Visitor &&visitor) {
172 int c1 = std::max(0, std::min(cols - 1, (int)(box.left * invCellW)));
173 int r1 = std::max(0, std::min(rows - 1, (int)(box.top * invCellH)));
174 int c2 = std::max(0, std::min(cols - 1, (int)(box.right * invCellW)));
175 int r2 = std::max(0, std::min(rows - 1, (int)(box.bottom * invCellH)));
176
177 for (int r = r1; r <= r2; ++r) {
178 int rowOffset = r * cols;
179 for (int c = c1; c <= c2; ++c) {
180 int nodeIdx = gridHead[rowOffset + c];
181 while (nodeIdx != -1) {
182 const auto &node = nodes[nodeIdx];
183 if (visitedToken[node.id] != cookie) {
184 visitedToken[node.id] = cookie;
185 visitor(node.id);
186 }
187 nodeIdx = node.next;
188 }
189 }
190 }
191 }
192};
193
219 public:
222 struct Candidate {
227 float area;
228 float invArea;
229 int16_t fontSize;
230 int16_t textAscent;
231 };
232
233 private:
234 struct LayoutItem {
235 int id;
236 LayoutBox objectBox;
237 uint32_t candStart;
238 uint16_t candCount;
239 int selectedRelIndex;
240 LayoutBox currentBox;
241 float currentArea;
242 float currentTotalCost;
243 };
244
245 LayoutConfig config;
246 int canvasWidth, canvasHeight;
247 std::function<TextSize(const std::string &, int)> measureFunc;
248
249 std::vector<LayoutItem> items;
250 std::vector<Candidate> candidatePool;
251 std::vector<int> processOrder;
252 FlatUniformGrid grid;
253 std::vector<int> visitedCookie;
254 int currentCookie = 0;
255 std::mt19937 rng;
256
257 public:
265 template <typename Func>
266 LabelLayoutSolver(int w, int h, Func &&func,
267 const LayoutConfig &cfg = LayoutConfig())
268 : config(cfg), canvasWidth(w), canvasHeight(h),
269 measureFunc(std::forward<Func>(func)), rng(12345) {
270 items.reserve(128);
271 candidatePool.reserve(4096);
272 visitedCookie.reserve(128);
273 }
274
276 void setConfig(const LayoutConfig &cfg) { config = cfg; }
277
279 void setCanvasSize(int w, int h) {
280 canvasWidth = w;
281 canvasHeight = h;
282 }
283
285 void clear() {
286 items.clear();
287 candidatePool.clear();
288 processOrder.clear();
289 }
290
298 void add(float l, float t, float r, float b, const std::string &text,
299 int baseFontSize) {
300 if (r - l < 2.0f) {
301 float cx = (l + r) * 0.5f;
302 l = cx - 1;
303 r = cx + 1;
304 }
305 if (b - t < 2.0f) {
306 float cy = (t + b) * 0.5f;
307 t = cy - 1;
308 b = cy + 1;
309 }
310
311 LayoutItem item;
312 item.id = (int)items.size();
313 item.objectBox = {std::floor(l), std::floor(t), std::ceil(r), std::ceil(b)};
314 item.candStart = (uint32_t)candidatePool.size();
315
316 generateCandidatesInternal(item, text, baseFontSize);
317 item.candCount = (uint16_t)(candidatePool.size() - item.candStart);
318
319 if (item.candCount > 0) {
320 item.selectedRelIndex = 0;
321 const auto &c = candidatePool[item.candStart];
322 item.currentBox = c.box;
323 item.currentArea = c.area;
324 item.currentTotalCost = c.geometricCost;
325 } else {
326 Candidate dummy;
327 dummy.box = {0, 0, 0, 0};
328 dummy.geometricCost = 1e9f;
329 dummy.staticCost = 0;
330 dummy.area = 0.1f;
331 dummy.invArea = 10.0f;
332 dummy.fontSize = (int16_t)baseFontSize;
333 dummy.textAscent = 0;
334 candidatePool.push_back(dummy);
335 item.candCount = 1;
336 item.selectedRelIndex = 0;
337 item.currentBox = dummy.box;
338 item.currentArea = 0.1f;
339 item.currentTotalCost = 1e9f;
340 }
341 items.push_back(std::move(item));
342 }
343
346 void solve() {
347 if (items.empty())
348 return;
349 const size_t N = items.size();
350
351 if (visitedCookie.size() < N)
352 visitedCookie.resize(N, 0);
353 bool useGrid = (N >= (size_t)config.spatialIndexThreshold);
354
355 if (useGrid) {
356 grid.resize(canvasWidth, canvasHeight, config.gridSize);
357 grid.clear();
358 for (const auto &item : items)
359 grid.insert(item.id, item.objectBox);
360 }
361
362 for (auto &item : items) {
363 float minCost = std::numeric_limits<float>::max();
364 int bestIdx = 0;
365
366 for (uint32_t i = 0; i < item.candCount; ++i) {
367 Candidate &cand = candidatePool[item.candStart + i];
368 float penalty = 0.0f;
369
370 auto checkStaticConflict = [&](int otherId) {
371 const auto &other = items[otherId];
372 if (LayoutBox::intersects(cand.box, other.objectBox)) {
373 float inter = LayoutBox::intersectArea(cand.box, other.objectBox);
374 penalty += (inter * cand.invArea) * config.costOccludeObj;
375 }
376 };
377
378 if (useGrid) {
379 currentCookie++;
380 grid.query(cand.box, visitedCookie, currentCookie,
381 checkStaticConflict);
382 } else {
383 for (const auto &other : items)
384 checkStaticConflict(other.id);
385 }
386 cand.staticCost = penalty;
387
388 float total = cand.geometricCost + cand.staticCost;
389 if (total < minCost) {
390 minCost = total;
391 bestIdx = (int)i;
392 }
393 }
394 item.selectedRelIndex = bestIdx;
395 const auto &bestCand = candidatePool[item.candStart + bestIdx];
396 item.currentBox = bestCand.box;
397 item.currentArea = bestCand.area;
398 item.currentTotalCost = minCost;
399 }
400
401 processOrder.resize(N);
402 for (size_t i = 0; i < N; ++i)
403 processOrder[i] = (int)i;
404
405 for (int iter = 0; iter < config.maxIterations; ++iter) {
406 std::shuffle(processOrder.begin(), processOrder.end(), rng);
407 int changeCount = 0;
408
409 if (useGrid) {
410 grid.clear();
411 for (const auto &item : items)
412 grid.insert(item.id, item.currentBox);
413 }
414
415 for (int idx : processOrder) {
416 auto &item = items[idx];
417
418 auto calculateDynamicCost = [&](const LayoutBox &box, float invBoxArea,
419 int selfId) -> float {
420 float overlapCost = 0.0f;
421 auto visitor = [&](int otherId) {
422 if (selfId == otherId)
423 return;
424 const auto &otherBox = items[otherId].currentBox;
425 if (LayoutBox::intersects(box, otherBox)) {
426 float inter = LayoutBox::intersectArea(box, otherBox);
427 overlapCost += (inter * invBoxArea) * config.costOverlapBase;
428 }
429 };
430 if (useGrid) {
431 currentCookie++;
432 grid.query(box, visitedCookie, currentCookie, visitor);
433 } else {
434 for (size_t j = 0; j < N; ++j)
435 visitor((int)j);
436 }
437 return overlapCost;
438 };
439
440 const auto &curCand =
441 candidatePool[item.candStart + item.selectedRelIndex];
442 float curDyn =
443 calculateDynamicCost(item.currentBox, curCand.invArea, item.id);
444 float currentRealTotal =
445 curCand.geometricCost + curCand.staticCost + curDyn;
446
447 if (currentRealTotal < 1.0f)
448 continue;
449
450 float bestIterCost = currentRealTotal;
451 int bestRelIdx = -1;
452
453 for (int i = 0; i < (int)item.candCount; ++i) {
454 if (i == item.selectedRelIndex)
455 continue;
456 const auto &cand = candidatePool[item.candStart + i];
457
458 float baseCost = cand.geometricCost + cand.staticCost;
459 if (baseCost >= bestIterCost)
460 continue;
461
462 float newOverlap =
463 calculateDynamicCost(cand.box, cand.invArea, item.id);
464 float newTotal = baseCost + newOverlap;
465
466 if (newTotal < bestIterCost) {
467 bestIterCost = newTotal;
468 bestRelIdx = i;
469 }
470 }
471
472 if (bestRelIdx != -1) {
473 item.selectedRelIndex = bestRelIdx;
474 const auto &newCand = candidatePool[item.candStart + bestRelIdx];
475 item.currentBox = newCand.box;
476 item.currentArea = newCand.area;
477 changeCount++;
478 }
479 }
480 if (changeCount == 0)
481 break;
482 }
483 }
484
487 std::vector<LayoutResult> getResults() const {
488 std::vector<LayoutResult> results;
489 results.reserve(items.size());
490 for (const auto &item : items) {
491 const auto &cand = candidatePool[item.candStart + item.selectedRelIndex];
492 results.push_back({cand.box.left, cand.box.top, (int)cand.fontSize,
493 (int)cand.box.width(), (int)cand.box.height(),
494 (int)cand.textAscent});
495 }
496 return results;
497 }
498
499 private:
500 void generateCandidatesInternal(LayoutItem &item, const std::string &text,
501 int baseFontSize) {
502 static const struct {
503 float scale;
504 int tier;
505 } levels[] = {{1.0f, 0}, {0.9f, 1}, {0.8f, 2}, {0.75f, 3}};
506
507 const auto &obj = item.objectBox;
508 for (const auto &lvl : levels) {
509 int fontSize = (int)(baseFontSize * lvl.scale);
510 if (fontSize < 9)
511 break;
512
513 TextSize ts = measureFunc(text, fontSize);
514 float fW = std::ceil((float)ts.width + config.paddingX * 2);
515 float fH =
516 std::ceil((float)(ts.height + ts.baseline + config.paddingY * 2));
517 float scalePenalty = lvl.tier * config.costScaleTier;
518 float area = fW * fH;
519 float invArea = 1.0f / (area > 0.1f ? area : 1.0f);
520
521 auto addCand = [&](float x, float y, float posCost) {
522 if (x < 0 || y < 0 || x + fW > canvasWidth || y + fH > canvasHeight)
523 return;
524 candidatePool.emplace_back();
525 auto &c = candidatePool.back();
526 c.box = {x, y, x + fW, y + fH};
527 c.geometricCost = posCost + scalePenalty;
528 c.staticCost = 0;
529 c.area = area;
530 c.invArea = invArea;
531 c.fontSize = (int16_t)fontSize;
532 c.textAscent = (int16_t)ts.height;
533 };
534
535 // Priority 1: Top (aligned left above)
536 addCand(obj.left, obj.top - fH, config.costPos1_Top);
537 // Priority 2: Right-Top (aligned top right)
538 addCand(obj.right, obj.top, config.costPos2_Right);
539 // Priority 3: Bottom (aligned left below)
540 addCand(obj.left, obj.bottom, config.costPos3_Bottom);
541 // Priority 4: Left-Top (aligned top left)
542 addCand(obj.left - fW, obj.top, config.costPos4_Left);
543
544 const float baseSlidePenalty = config.costSlidingPenalty;
545
546 auto getDynamicSteps = [](float rangeSize) {
547 return std::clamp((int)(rangeSize / 40.0f), 3, 15);
548 };
549
550 float rangeX = std::max(0.0f, obj.right - fW - obj.left);
551 if (rangeX > 1.0f) {
552 int stepsX = getDynamicSteps(rangeX);
553 float invStepsX = 1.0f / (float)stepsX;
554 for (int i = 1; i < stepsX; ++i) {
555 float r = i * invStepsX;
556 float x = obj.left + rangeX * r;
557 float penalty = baseSlidePenalty + (r * 10.0f);
558 addCand(x, obj.top - fH, config.costPos1_Top + penalty);
559 addCand(x, obj.bottom, config.costPos3_Bottom + penalty);
560 }
561 }
562
563 float rangeY = std::max(0.0f, obj.bottom - fH - obj.top);
564 if (rangeY > 1.0f) {
565 int stepsY = getDynamicSteps(rangeY);
566 float invStepsY = 1.0f / (float)stepsY;
567 for (int i = 1; i < stepsY; ++i) {
568 float r = i * invStepsY;
569 float y = obj.top + rangeY * r;
570 float penalty = baseSlidePenalty + (r * 10.0f);
571 addCand(obj.right, y, config.costPos2_Right + penalty);
572 addCand(obj.left - fW, y, config.costPos4_Left + penalty);
573 }
574 }
575 }
576 }
577};
578
579} // namespace reusex::vision::osd
Compact spatial hash-grid for fast overlap queries during layout.
void query(const LayoutBox &box, std::vector< int > &visitedToken, int cookie, Visitor &&visitor)
Invoke visitor for each unique item overlapping box.
void resize(int w, int h, int gridSize)
Resize the grid to cover a canvas of w×h pixels using cells of gridSize.
void clear()
Reset all cells, keeping allocated memory.
void insert(int id, const LayoutBox &box)
Insert item id with the given box into the grid.
std::vector< LayoutResult > getResults() const
Retrieve the placement result for each label in insertion order.
void solve()
Run the iterative placement optimisation.
void add(float l, float t, float r, float b, const std::string &text, int baseFontSize)
Register a new label to be placed.
void setCanvasSize(int w, int h)
Change the canvas dimensions (e.g. after a resize).
void setConfig(const LayoutConfig &cfg)
Update the layout configuration.
void clear()
Remove all items from the solver (allows reuse across frames).
LabelLayoutSolver(int w, int h, Func &&func, const LayoutConfig &cfg=LayoutConfig())
Construct a solver for a canvas of the given dimensions.
Internal representation of a single placement candidate for a label.
float invArea
Reciprocal of area (cached for performance).
float geometricCost
Cost based on anchor preference and font scale.
LayoutBox box
Candidate label box on the canvas.
float area
Box area (cached for performance).
int16_t fontSize
Font size for this candidate.
int16_t textAscent
Text ascent (for putText offset calculation).
float staticCost
Cost from overlap with object boxes (computed once).
Axis-aligned 2-D bounding box used internally by the label layout solver.
float width() const
Returns the width of the box.
float height() const
Returns the height of the box.
static float intersectArea(const LayoutBox &box1, const LayoutBox &box2)
Computes the intersection area of two LayoutBoxes.
float area() const
Returns the area of the box (clamped to 0 for inverted boxes).
static bool intersects(const LayoutBox &box1, const LayoutBox &box2)
Returns true when two LayoutBoxes overlap.
float bottom
Bottom edge y-coordinate.
float top
Top edge y-coordinate.
float left
Left edge x-coordinate.
float right
Right edge x-coordinate.
Tunable parameters controlling the label placement algorithm.
float costScaleTier
Cost multiplier per font-size scale tier (keep large to prefer position changes over size reduction).
float costOverlapBase
Base penalty per unit overlap with another label.
int paddingY
Vertical padding around each label in pixels.
float costPos1_Top
Base geometric cost for anchor position 1 (top of object — preferred).
int spatialIndexThreshold
Minimum number of items before a grid index is used.
int maxIterations
Maximum refinement iterations (higher = better quality).
int gridSize
Spatial-index cell size in pixels.
float costPos2_Right
Base geometric cost for anchor position 2 (right of object).
float costSlidingPenalty
Extra cost for a sliding (non-anchor) candidate. Must be > costPos4_Left.
float costPos4_Left
Base geometric cost for anchor position 4 (left of object).
float costOccludeObj
Penalty per unit overlap with an object bounding box.
float costPos3_Bottom
Base geometric cost for anchor position 3 (bottom of object).
int paddingX
Horizontal padding around each label in pixels.
Final placement result for a single label.
float y
Top edge of the placed label box.
int textAscent
Ascent value to use when calling putText.
int height
Height of the label box in pixels.
int width
Width of the label box in pixels.
float x
Left edge of the placed label box.
int fontSize
Font size chosen for this label (may be scaled down).
Measured bounding box of a rendered text string.
int width
Rendered string width in pixels.
int baseline
Descent below the baseline in pixels.
int height
Rendered string height (ascent) in pixels.