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