img_phy_sim.ray_tracing
Beam Tracing and Visualization Utilities
This module provides a set of tools for simulating and visualizing the propagation of rays (beams) through 2D images that may contain reflective or obstructive walls. It includes methods for tracing ray paths, handling reflections, scaling and normalizing ray coordinates, and drawing the resulting beam paths onto images.
The core idea is to represent a 2D environment as an image where certain pixel values correspond to walls or obstacles. Rays are emitted from a relative position and traced in specified directions, optionally reflecting off walls multiple times. The results can then be visualized or further processed.
Main features:
- Ray tracing with customizable reflection order and wall detection
- Support for relative and absolute coordinate systems
- Ray scaling utilities for normalization or resizing
- Image rendering functions to visualize rays, walls, and reflection paths
- Flexible output modes: single image, multi-channel, or multiple separate images
Typical workflow:
- Use
trace_beams()to simulate multiple beams across an image. - Render the rays on an image using
draw_rays().
Dependencies:
- numpy
- cv2 (OpenCV)
- internal math + img modules
Example:
img = ips.img.open("scene.png")
rays = ips.ray_tracing.trace_beams(
rel_position=(0.5, 0.5),
img_src=img,
directions_in_degree=[0, 45, 90, 135],
wall_values=[0],
wall_thickness=2,
reflexion_order=2
)
output = ips.ray_tracing.draw_rays(rays, img_shape=img.shape, ray_value=255, ray_thickness=1)
ips.img.imshow(output, size=5)
Functions:
- print_rays_info(...) - Pritn informations about created rays.
- save(...) - Save rays into a file.
- open(...) - Load saved rays.
- merge(...) - Merge 2 or more rays together.
- get_all_pixel_coordinates_in_between(...) - Use brahams algorithm for getting any line in a quantizied space.
- get_wall_map(...) - Extract edges and get the wall-map with direction angles of walls.
- update_pixel_position(...) - Get the next pixel to come from one point to another in a quantizied system.
- calc_reflection(...) - Calculate the reflexion of 2 vectors.
- get_img_border_vector(...) - Get the vector of a border of the image.
- trace_beam(...) - Trace a single beam with reflexions.
- trace_beams(...) - Trace multiple beams with reflections through an image.
- scale_rays(...) - Normalize or rescale ray coordinates.
- draw_rectangle_with_thickness(...) - Draw filled or thick rectangles.
- draw_line_or_point(...) - Draw a single point or a line segment.
- draw_rays(...) - Visualize traced rays as images or channels.
- trace_and_draw_rays(...) - Compute and draw rays.
1""" 2**Beam Tracing and Visualization Utilities** 3 4This module provides a set of tools for simulating and visualizing the propagation 5of rays (beams) through 2D images that may contain reflective or obstructive walls. 6It includes methods for tracing ray paths, handling reflections, scaling and 7normalizing ray coordinates, and drawing the resulting beam paths onto images. 8 9The core idea is to represent a 2D environment as an image where certain pixel 10values correspond to walls or obstacles. Rays are emitted from a relative position 11and traced in specified directions, optionally reflecting off walls multiple times. 12The results can then be visualized or further processed. 13 14Main features: 15- Ray tracing with customizable reflection order and wall detection 16- Support for relative and absolute coordinate systems 17- Ray scaling utilities for normalization or resizing 18- Image rendering functions to visualize rays, walls, and reflection paths 19- Flexible output modes: single image, multi-channel, or multiple separate images 20 21Typical workflow: 221. Use `trace_beams()` to simulate multiple beams across an image. 232. Render the rays on an image using `draw_rays()`. 24 25Dependencies: 26- numpy 27- cv2 (OpenCV) 28- internal math + img modules 29 30Example: 31```python 32img = ips.img.open("scene.png") 33rays = ips.ray_tracing.trace_beams( 34 rel_position=(0.5, 0.5), 35 img_src=img, 36 directions_in_degree=[0, 45, 90, 135], 37 wall_values=[0], 38 wall_thickness=2, 39 reflexion_order=2 40) 41output = ips.ray_tracing.draw_rays(rays, img_shape=img.shape, ray_value=255, ray_thickness=1) 42ips.img.imshow(output, size=5) 43``` 44 45Functions: 46- print_rays_info(...) - Pritn informations about created rays. 47- save(...) - Save rays into a file. 48- open(...) - Load saved rays. 49- merge(...) - Merge 2 or more rays together. 50- get_all_pixel_coordinates_in_between(...) - Use brahams algorithm for getting any line in a quantizied space. 51- get_wall_map(...) - Extract edges and get the wall-map with direction angles of walls. 52- update_pixel_position(...) - Get the next pixel to come from one point to another in a quantizied system. 53- calc_reflection(...) - Calculate the reflexion of 2 vectors. 54- get_img_border_vector(...) - Get the vector of a border of the image. 55- trace_beam(...) - Trace a single beam with reflexions. 56- trace_beams(...) - Trace multiple beams with reflections through an image. 57- scale_rays(...) - Normalize or rescale ray coordinates. 58- draw_rectangle_with_thickness(...) - Draw filled or thick rectangles. 59- draw_line_or_point(...) - Draw a single point or a line segment. 60- draw_rays(...) - Visualize traced rays as images or channels. 61- trace_and_draw_rays(...) - Compute and draw rays. 62""" 63 64 65 66# --------------- 67# >>> Imports <<< 68# --------------- 69from .img import open as img_open, get_width_height 70from .math import degree_to_vector, vector_to_degree, normalize_point, \ 71 degree_to_vector_numba, vector_to_degree_numba, normalize_point_numba 72 73import builtins 74import pickle 75import math 76import copy 77 78import numpy as np 79import cv2 80 81# performance optimization 82from joblib import Parallel, delayed 83import numba 84from numba.typed import List 85from numba import types 86 87 88 89# -------------- 90# >>> Helper <<< 91# -------------- 92 93class RayIterator: 94 """ 95 A container class to save every step of a ray tracing process. 96 """ 97 def __init__(self, other_ray_iterator=None): 98 """ 99 Initialize a RayIterator instance. 100 101 Parameters: 102 - other_ray_iterator (RayIterator, optional): <br> 103 An existing RayIterator to copy. If provided, creates a deep copy 104 of the other iterator's rays_collection. If None, creates an empty iterator. 105 106 Returns: <br> 107 None 108 """ 109 if other_ray_iterator is None: 110 self.rays_collection = [] 111 else: 112 self.rays_collection = copy.deepcopy(other_ray_iterator.rays_collection) 113 114 def __repr__(self): 115 """ 116 Return a string representation of the RayIterator. 117 118 Returns:<br> 119 str: String representation showing the number of iterations/time-steps. 120 """ 121 return f"RayIterator with {self.len_iterations()} iterations (time-steps)." 122 123 def __iter__(self): 124 """ 125 Make the RayIterator iterable over its rays collections. 126 127 Yields:<br> 128 list: Each iteration's collection of rays. 129 """ 130 for rays in self.rays_collection: 131 yield rays 132 133 def __getitem__(self, key): 134 """ 135 Get rays from the latest iteration using key/index. 136 137 Parameters: 138 - key (int or slice): Index or slice to access rays in the latest iteration. 139 140 Returns:<br> 141 list: Rays from the latest iteration corresponding to the key. 142 """ 143 return self.rays_collection[-1][key] 144 145 def __len__(self): 146 """ 147 Get the number of rays in the latest iteration. 148 149 Returns:<br> 150 int: Number of rays in the latest iteration. 151 """ 152 return len(self.rays_collection[-1]) 153 154 def __add__(self, other): 155 """ 156 Add another RayIterator or value to this RayIterator element-wise. 157 158 Parameters: 159 - other (RayIterator or any): <br> 160 If RayIterator: combines ray collections from both iterators. 161 If other type: adds the value to each ray in all iterations. 162 163 Returns:<br> 164 RayIterator: New RayIterator containing the combined/adjusted results. 165 166 Raises:<br> 167 TypeError: If other is not a RayIterator and addition operation fails. 168 """ 169 if isinstance(other, RayIterator): 170 new_iterator = RayIterator() 171 if self.len_iterations() > other.len_iterations(): 172 iter_1 = self 173 iter_2 = other 174 else: 175 iter_1 = other 176 iter_2 = self 177 178 iter_1 = copy.deepcopy(iter_1) 179 iter_2 = copy.deepcopy(iter_2) 180 181 for idx in range(iter_1.len_iterations()): 182 cur_addition = iter_1.get_iteration(idx) 183 if iter_2.len_iterations() > idx: 184 cur_addition += iter_2.get_iteration(idx) 185 elif iter_2.len_iterations() == 0: 186 pass 187 else: 188 cur_addition += iter_2.get_iteration(-1) 189 190 new_iterator.add_iteration(cur_addition) 191 192 return new_iterator 193 else: 194 new_iterator = RayIterator() 195 196 for idx in range(self.len_iterations()): 197 cur_addition = self.get_iteration(idx) + other 198 new_iterator.add_iteration(cur_addition) 199 200 return new_iterator 201 202 def __iadd__(self, other): 203 """ 204 In-place addition of another RayIterator or value. 205 206 Parameters: 207 - other (RayIterator or any): 208 Object to add to this RayIterator. 209 210 Returns:<br> 211 RayIterator: self, after performing the in-place addition. 212 """ 213 new_iterator = self.__add__(other) 214 self.rays_collection = new_iterator.rays_collection 215 return self 216 217 def len_iterations(self): 218 """ 219 Get the total number of iterations/time-steps stored. 220 221 Returns:<br> 222 int: <br> 223 Number of iterations in the rays_collection. 224 """ 225 return len(self.rays_collection) 226 227 def add_iteration(self, rays): 228 """ 229 Add a new iteration (collection of rays) to the iterator. 230 231 Parameters: 232 - rays (list): Collection of rays to add as a new iteration. 233 Format: rays[ray][beam][point] = (x, y) 234 235 Returns:<br> 236 RayIterator: self, for method chaining. 237 """ 238 self.rays_collection += [copy.deepcopy(rays)] 239 return self 240 241 def add_rays(self, rays): 242 """ 243 Add rays to the every iteration (in-place modification). 244 If one iterator have less steps, the last step will be used for all other iterations. 245 Which equals no change for those iterations. 246 247 Parameters: 248 - rays (list): Rays to add to the latest iteration. 249 Format: rays[ray][beam][point] = (x, y) 250 251 Returns:<br> 252 list: <br> 253 The updated rays_collection. 254 """ 255 self.rays_collection = self.__add__(copy.deepcopy(rays)).rays_collection 256 return self.rays_collection 257 258 def print_info(self): 259 """ 260 Print statistical information about the ray collections. 261 262 Displays: 263 - Number of iterations 264 - Information about the latest iteration's rays including: 265 * Number of rays, beams, reflexions, and points 266 * Mean, median, max, min, and variance for beams per ray, 267 reflexions per ray, and points per beam 268 * Value range for x and y coordinates 269 270 Returns:<br> 271 None 272 """ 273 print(f"Ray Iterator with {self.len_iterations()} iterations (time-steps).") 274 print("Latest Rays Info:\n") 275 print_rays_info(self.rays_collection[-1]) 276 # for idx, rays in enumerate(self.rays_collection): 277 # print(f"--- Rays Set {idx} ---") 278 # print_rays_info(rays) 279 280 def reduce_to_x_steps(self, x_steps): 281 """ 282 Reduce the number of stored iterations to approximately x_steps. 283 284 Uses linear sampling to keep representative iterations while reducing 285 memory usage. If x_steps is greater than current iterations, does nothing. 286 287 Parameters: 288 - x_steps (int): <br> 289 Desired number of iterations to keep. 290 291 Returns:<br> 292 None 293 """ 294 if x_steps >= self.len_iterations(): 295 return # nothing to do 296 297 step_size = self.len_iterations() / x_steps 298 new_rays_collection = [] 299 for i in range(x_steps): 300 index = int(i * step_size) 301 new_rays_collection += [self.get_iteration(index)] 302 303 self.rays_collection = new_rays_collection 304 305 def apply_and_update(self, func): 306 """ 307 Apply a function to each iteration's rays and update in-place. 308 309 Parameters: 310 - func (callable): <br> 311 Function that takes a rays collection and returns a modified rays collection. 312 Will be applied to each iteration. 313 314 Returns:<br> 315 None 316 """ 317 for i in range(self.len_iterations()): 318 self.rays_collection[i] = func(self.rays_collection[i]) 319 320 def apply_and_return(self, func): 321 """ 322 Apply a function to each iteration's rays and return results. 323 324 Parameters: 325 - func (callable): <br> 326 Function that takes a rays collection and returns some result. 327 Will be applied to each iteration. 328 329 Returns: 330 list: <br> 331 Results of applying func to each iteration's rays. 332 """ 333 results = [] 334 for i in range(self.len_iterations()): 335 results += [func(self.rays_collection[i])] 336 337 return results 338 339 def get_iteration(self, index): 340 """ 341 Get a specific iteration's rays collection. 342 343 Parameters: 344 - index (int): <br> 345 Index of the iteration to retrieve. Supports negative indexing (e.g., -1 for last iteration). 346 347 Returns: 348 list: <br> 349 - Rays collection at the specified iteration. 350 351 Raises: <br> 352 IndexError: If index is out of range. 353 """ 354 if index < -1 * self.len_iterations() or index >= self.len_iterations(): 355 raise IndexError("RayIterator index out of range.") 356 return self.rays_collection[index] 357 358# def convert_rays_to_ray_iterator(rays): 359# """ 360# Convert a list of rays into a RayIterator with a single iteration. 361 362# Parameters: 363# - rays (list): 364# Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y) 365# Returns: 366# - RayIterator: 367# RayIterator containing the provided rays as its only iteration. 368# """ 369# ray_iterator = RayIterator() 370# cur_rays = [] 371 372# max_iterations 373 374# for ray_idx in range(len(rays)): 375# max_beams = max([len(beams) for beams in rays[ray_idx]]) 376# for cur_beam_idx in range(max_beams): 377# ray = [] 378# for point in rays[ray_idx][cur_beam_idx]: 379# cur_rays += [ray] 380# ray_iterator.add_iteration(cur_rays) 381# return ray_iterator 382 383def print_rays_info(rays): 384 """ 385 Print statistical information about a collection of rays. 386 387 Each ray consists of multiple beams, and each beam consists of multiple points. 388 The function computes and displays statistics such as: 389 - Number of rays, beams, reflexions, and points 390 - Mean, median, max, min, and variance for beams per ray, reflexions per ray, and points per beam 391 - Value range for x and y coordinates 392 393 Parameters: 394 - rays (list): <br> 395 Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y) 396 """ 397 if isinstance(rays, RayIterator): 398 rays.print_info() 399 else: 400 nrays = 0 401 nbeams = 0 402 nbeams_per_ray = [] 403 nreflexions = 0 404 nreflexions_per_ray = [] 405 npoints = 0 406 npoints_per_beam_point = [] 407 values_per_point = [] 408 min_x_value = None 409 max_x_value = None 410 min_y_value = None 411 max_y_value = None 412 for ray in rays: 413 nrays += 1 414 cur_beams = 0 415 cur_reflexions = 0 416 for beam_points in ray: 417 nbeams += 1 418 nreflexions += 1 419 cur_beams += 1 420 cur_reflexions += 1 421 cur_points = 0 422 for x in beam_points: 423 npoints += 1 424 cur_points += 1 425 values_per_point += [len(x)] 426 min_x_value = x[0] if min_x_value is None else min(min_x_value, x[0]) 427 max_x_value = x[0] if max_x_value is None else max(max_x_value, x[0]) 428 min_y_value = x[1] if min_y_value is None else min(min_y_value, x[1]) 429 max_y_value = x[1] if max_y_value is None else max(max_y_value, x[1]) 430 npoints_per_beam_point += [cur_points] 431 nreflexions -= 1 432 cur_reflexions -= 1 433 nreflexions_per_ray += [cur_reflexions] 434 nbeams_per_ray += [cur_beams] 435 436 print(f"Rays: {nrays}") 437 print(f"Beams: {nbeams}") 438 print(f" - Mean Beams per Ray: {round(np.mean(nbeams_per_ray), 1)}") 439 print(f" - Median: {round(np.median(nbeams_per_ray), 1)}") 440 print(f" - Max: {round(np.max(nbeams_per_ray), 1)}") 441 print(f" - Min: {round(np.min(nbeams_per_ray), 1)}") 442 print(f" - Variance: {round(np.std(nbeams_per_ray), 1)}") 443 print(f"Reflexions: {nreflexions}") 444 print(f" - Mean Reflexions per Ray: {round(np.mean(nreflexions_per_ray), 1)}") 445 print(f" - Median: {round(np.median(nreflexions_per_ray), 1)}") 446 print(f" - Max: {round(np.max(nreflexions_per_ray), 1)}") 447 print(f" - Min: {round(np.min(nreflexions_per_ray), 1)}") 448 print(f" - Variance: {round(np.std(nreflexions_per_ray), 1)}") 449 print(f"Points: {npoints}") 450 print(f" - Mean Points per Beam: {round(np.mean(npoints_per_beam_point), 1)}") 451 print(f" - Median: {round(np.median(npoints_per_beam_point), 1)}") 452 print(f" - Max: {round(np.max(npoints_per_beam_point), 1)}") 453 print(f" - Min: {round(np.min(npoints_per_beam_point), 1)}") 454 print(f" - Variance: {round(np.std(npoints_per_beam_point), 1)}") 455 print(f" - Mean Point Values: {round(np.mean(values_per_point), 1)}") 456 print(f" - Median: {round(np.median(values_per_point), 1)}") 457 print(f" - Variance: {round(np.std(values_per_point), 1)}") 458 print(f"\nValue-Range:\n x ∈ [{min_x_value:.2f}, {max_x_value:.2f}]\n y ∈ [{min_y_value:.2f}, {max_y_value:.2f}]") 459 # [ inclusive, ( number is not included 460 461 if nrays > 0: 462 print(f"\nExample:\nRay 1, beams: {len(rays[0])}") 463 if nbeams > 0: 464 print(f"Ray 1, beam 1, points: {len(rays[0][0])}") 465 if npoints > 0: 466 print(f"Ray 1, beam 1, point 1: {len(rays[0][0][0])}") 467 print("\n") 468 469 470 471def save(path, rays): 472 """ 473 Save a list of rays to a text file. 474 475 The rays are serialized using a simple text-based format. 476 Each ray is delimited by '>' and '<', and each point is represented as "x | y". 477 478 Parameters: 479 - path (str): <br> 480 Path to the file where data should be saved. If no '.txt' extension is present, it will be appended automatically. 481 - rays (list): <br> 482 Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y) 483 484 Returns:<br> 485 None 486 """ 487 if isinstance(rays, RayIterator): 488 if not path.endswith(".pkl"): 489 path += ".pkl" 490 pickle.dump(rays, builtins.open(path, "wb")) 491 return 492 493 # transform rays into an string 494 rays_str = "" 495 for ray in rays: 496 rays_str += ">\n" 497 for beam in ray: 498 rays_str += "\n" 499 for cur_point in beam: 500 rays_str += f"{cur_point[0]} | {cur_point[1]}, " 501 rays_str += "<\n" 502 503 rays_str = rays_str.replace("\n\n", "\n") 504 505 if not path.endswith(".txt"): 506 path += ".txt" 507 508 with builtins.open(path, "w") as file_: 509 file_.write(rays_str) 510 511 512 513def open(path, is_iterator=False) -> list: 514 """ 515 Open and parse a ray text file into a structured list. 516 517 The file is expected to follow the same format as produced by `save()`. 518 519 Parameters: 520 - path (str): <br> 521 Path to the .txt file containing ray data. 522 523 Returns: 524 - list: <br> 525 Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y) 526 """ 527 if is_iterator or path.endswith(".pkl"): 528 if not path.endswith(".pkl"): 529 path += ".pkl" 530 rays = pickle.load(builtins.open(path, "rb")) 531 return rays 532 533 if not path.endswith(".txt"): 534 path += ".txt" 535 536 with builtins.open(path, "r") as file_: 537 content = file_.read().strip() 538 539 rays = [] 540 for ray in content.split(">"): 541 extracted_ray = [] 542 for beam in ray.split("\n"): 543 extracted_beam = [] 544 beam = beam.strip() 545 if not beam or beam == "<": 546 continue 547 548 for point in beam.split(","): 549 point = point.strip() 550 if not point or point == "<": 551 continue 552 553 try: 554 point_x, point_y = point.strip().split("|") 555 except Exception as e: 556 print("Point of error:", point) 557 raise e 558 559 extracted_beam += [(float(point_x), float(point_y))] 560 if len(extracted_beam) > 0: 561 extracted_ray += [extracted_beam] 562 if len(extracted_ray) > 0: 563 rays += [extracted_ray] 564 return rays 565 566 567 568def merge(rays_1, rays_2, *other_rays_): 569 """ 570 Merge multiple ray datasets into a single list. 571 572 Parameters: 573 - rays_1 (list): <br> 574 First set of rays. 575 - rays_2 (list): <br> 576 Second set of rays. 577 - *other_rays_ (list): <br> 578 Additional ray lists to merge. 579 580 Returns: 581 - list: 582 Combined list of all rays. 583 """ 584 merged = rays_1 + rays_2 585 586 for rays in other_rays_: 587 merged += rays 588 589 return merged 590 591 592 593 594# ---------------------- 595# >>> Core Functions <<< 596# ---------------------- 597 598def get_all_pixel_coordinates_in_between(x1, y1, x2, y2): 599 """ 600 Get all pixel coordinates along a line between two points using Bresenham's algorithm. 601 602 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 603 604 Parameters: 605 - x1 (int): <br> 606 Starting x-coordinate. 607 - y1 (int): <br> 608 Starting y-coordinate. 609 - x2 (int): <br> 610 Ending x-coordinate. 611 - y2 (int): <br> 612 Ending y-coordinate. 613 614 Returns: 615 - list: 616 List of (x, y) tuples representing all pixels between the start and end points 617 """ 618 coordinates = [] 619 620 dx = abs(x2 - x1) 621 dy = abs(y2 - y1) 622 x, y = x1, y1 623 624 sx = 1 if x1 < x2 else -1 625 sy = 1 if y1 < y2 else -1 626 627 if dx > dy: 628 err = dx / 2.0 629 while x != x2: 630 coordinates += [(x, y)] 631 err -= dy 632 if err < 0: 633 y += sy 634 err += dx 635 x += sx 636 else: 637 err = dy / 2.0 638 while y != y2: 639 coordinates += [(x, y)] 640 err -= dx 641 if err < 0: 642 x += sx 643 err += dy 644 y += sy 645 646 coordinates += [(x2, y2)] # include the last point 647 return coordinates 648 649 650 651def get_wall_map(img, wall_values=None, thickness=1, 652 use_numba_compilation=False): 653 """ 654 Generate a wall map where each pixel encodes the wall orientation (in degrees). 655 656 Parameters: 657 - img (numpy.ndarray): <br> 658 Input image representing scene or segmentation mask. 659 - wall_values (list, optional): <br> 660 Specific pixel values considered as walls. If None, all non-zero pixels are treated as walls. 661 - thickness (int, optional): <br> 662 Thickness of wall lines (default is 1). 663 - use_numba_compilation (bool, optional):<br> 664 Whether to use the compiled (to machine code) version of compute heavy functions. 665 666 Returns: 667 - numpy.ndarray: 668 2D array (same width and height as input) 669 where each wall pixel contains the wall angle in degrees (0-360), 670 and non-wall pixels are set to infinity (np.inf). 671 """ 672 # numba optimization -> change function locally 673 if use_numba_compilation: 674 get_all_pixel_coordinates_in_between_ = get_all_pixel_coordinates_in_between_numba 675 else: 676 get_all_pixel_coordinates_in_between_ = get_all_pixel_coordinates_in_between 677 678 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 679 wall_map = np.full((IMG_HEIGHT, IMG_WIDTH), np.inf, dtype=np.uint16) # uint16 to get at least 360 degree/value range 680 681 # only detect edges from objects with specific pixel values 682 if wall_values is not None: 683 mask = np.isin(img, wall_values).astype(np.uint8) * 255 684 else: 685 mask = img 686 if np.max(mask) < 64: 687 mask = mask.astype(np.uint8) * 255 688 689 # detect edges and contours 690 edges = cv2.Canny(mask, 100, 200) 691 contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 692 693 # convert contours to line segments 694 for c in contours: 695 for i in range(len(c)-1): 696 x1, y1 = c[i][0] 697 x2, y2 = c[i+1][0] 698 dy = y2 - y1 699 dx = x2 - x1 700 angle = math.atan2(dy, dx) 701 angle_deg = math.degrees(angle) 702 for x, y in get_all_pixel_coordinates_in_between_(x1, y1, x2, y2): 703 # wall_map[y, x] = int(angle_deg) % 360 704 705 for tx in range(-thickness, thickness+1): 706 for ty in range(-thickness, thickness+1): 707 nx, ny = x+tx, y+ty 708 if 0 <= nx < IMG_WIDTH and 0 <= ny < IMG_HEIGHT: 709 wall_map[ny, nx] = int(angle_deg) % 360 710 return wall_map 711 712 713 714def update_pixel_position(direction_in_degree, cur_position, target_line): 715 """ 716 Update the pixel position of a moving point toward a target line based on direction and proximity. 717 718 Combines the direction vector with a vector pointing toward the closest point 719 on the target line, ensuring pixel-wise movement (discrete steps). 720 721 Parameters: 722 - direction_in_degree (float): <br> 723 Movement direction in degrees. 724 - cur_position (tuple): <br> 725 Current pixel position (x, y). 726 - target_line (list): <br> 727 Target line defined as [x1, y1, x2, y2]. 728 729 Returns: 730 - tuple: 731 Updated pixel position (x, y). 732 """ 733 # 1. Calc distance from point to target line 734 735 # perpendicular vector to line (points toward line) 736 point = np.array(cur_position) 737 line_start_point = np.array(target_line[0:2]) 738 line_end_point = np.array(target_line[2:4]) 739 740 # projection along the line -> throw the point vector vertical/perpendicular on the line and see where it cuts with normed AP to AB 741 # t is the length from point to the line, therefore it gets normed 742 t = np.dot(point - line_start_point, line_end_point - line_start_point) / (np.dot(line_end_point - line_start_point, line_end_point - line_start_point) + 1e-8) 743 744 # limit it to the line id needed -> because we don't want smaller or bigger values than that 745 # -> 0 would be point A 746 # -> 1 would be point B 747 # t = np.clip(t, 0, 1) 748 if t < 0.0: 749 t = 0.0 750 elif t > 1.0: 751 t = 1.0 752 753 # get closest point by applying the found t as lentgh from startpoint in the line vector direction 754 closest = line_start_point + t * (line_end_point - line_start_point) 755 756 # get the final vector to the line 757 to_line = closest - point # vector from current pos to closest point on line 758 759 # 2. Calc vector to the degree 760 # movement vector based on angle 761 rad = math.radians(direction_in_degree) 762 move_dir = np.array([math.cos(rad), math.sin(rad)]) 763 764 # 3. Combine vector to the line and degree vector 765 # combine movement towards direction and towards line 766 combined = move_dir + to_line * 0.5 # weighting factor 767 768 # pick pixel step (continuous to discrete) -> [-1, 0, 1] 769 step_x = np.sign(combined[0]) 770 step_y = np.sign(combined[1]) 771 772 # clamp to [-1, 1], if bigger/smaller 773 # step_x = int(np.clip(step_x, -1, 1)) 774 if step_x < 0.0: 775 step_x = 0.0 776 elif step_x > 1.0: 777 step_x = 1.0 778 # step_y = int(np.clip(step_y, -1, 1)) 779 if step_y < 0.0: 780 step_y = 0.0 781 elif step_y > 1.0: 782 step_y = 1.0 783 784 return (int(cur_position[0] + step_x), int(cur_position[1] + step_y)) 785 786 787 788def calc_reflection(collide_vector, wall_vector): 789 """ 790 Calculate the reflection of a collision vector against a wall vector. 791 792 The reflection is computed using the wall's normal vector and the formula: 793 r = v - 2 * (v · n) * n 794 795 Parameters: 796 - collide_vector (array-like): <br> 797 Incoming vector (2D). 798 - wall_vector (array-like): <br> 799 Wall direction vector (2D). 800 801 Returns: 802 - numpy.ndarray: 803 Reflected 2D vector. 804 """ 805 # normalize both 806 collide_vector = np.array(collide_vector, dtype=np.float64) 807 collide_vector /= np.linalg.norm(collide_vector) 808 wall_vector = np.array(wall_vector, dtype=np.float64) 809 wall_vector /= np.linalg.norm(wall_vector) 810 811 # calculate the normal of the wall 812 normal_wall_vector_1 = np.array([-wall_vector[1], wall_vector[0]]) # rotated +90° 813 normal_wall_vector_2 = np.array([wall_vector[1], -wall_vector[0]]) # rotated -90° 814 815 # decide which vector is the right one 816 # -> dot product tells which normal faces the incoming vector 817 # -> dor product shows how similiar 2 vectors are => smaller 0 means they show against each other => right vector 818 if np.dot(collide_vector, normal_wall_vector_1) < 0: 819 normal_wall_vector = normal_wall_vector_1 820 else: 821 normal_wall_vector = normal_wall_vector_2 822 823 # calc the reflection 824 return collide_vector - 2 * np.dot(collide_vector, normal_wall_vector) * normal_wall_vector 825 826 827def get_img_border_vector(position, max_width, max_height): 828 """ 829 Determine the wall normal vector for an image border collision. 830 831 Parameters: 832 - position (tuple): <br> 833 Current position (x, y). 834 - max_width (int): <br> 835 Image width. 836 - max_height (int): <br> 837 Image height. 838 839 Returns: 840 - tuple: 841 Border wall vector corresponding to the collision side. 842 """ 843 # print(f"got {position=}") 844 if position[0] < 0: 845 return (0, 1) 846 elif position[0] >= max_width: 847 return (0, 1) 848 elif position[1] < 0: 849 return (1, 0) 850 elif position[1] >= max_height: 851 return (1, 0) 852 else: 853 # should never reach that! 854 return (0, 0) 855 856 857def trace_beam(abs_position, 858 img, 859 direction_in_degree, 860 wall_map, 861 wall_values, 862 img_border_also_collide=False, 863 reflexion_order=3, 864 should_scale=True, 865 should_return_iterative=False, 866 remove_iterative=True): 867 """ 868 Trace a ray (beam) through an image with walls and reflections. 869 870 The beam starts from a given position and follows a direction until it hits 871 a wall or border. On collisions, reflections are computed using wall normals. 872 873 Parameters: 874 - abs_position (tuple): <br> 875 Starting position (x, y) of the beam. 876 - img (numpy.ndarray): <br> 877 Input image or segmentation map. 878 - direction_in_degree (float): <br> 879 Initial direction angle of the beam. 880 - wall_map (numpy.ndarray): <br> 881 Map containing wall orientations in degrees. 882 - wall_values (list): <br> 883 List of pixel values representing walls. 884 - img_border_also_collide (bool, optional): <br> 885 Whether the image border acts as a collider (default: False). 886 - reflexion_order (int, optional): <br> 887 Number of allowed reflections (default: 3). 888 - should_scale (bool, optional): <br> 889 Whether to normalize positions to [0, 1] (default: True). 890 - should_return_iterative (bool, optional): <br> 891 Whether to return a RayIterator for step-by-step analysis (default: False). 892 - remove_iterative (bool, optional): <br> 893 Whether to optimize (ignore) ray-iterator -> only in use if should_return_iterative is False. 894 895 Returns: 896 - list: 897 Nested list structure representing the traced ray and its reflections. 898 Format: ray[beam][point] = (x, y) 899 """ 900 reflexion_order += 1 # Reflexion Order == 0 means, no reflections, therefore only 1 loop 901 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 902 903 ray = [] 904 if should_return_iterative or not remove_iterative: 905 ray_iterator = RayIterator() 906 907 cur_target_abs_position = abs_position 908 cur_direction_in_degree = direction_in_degree % 360 909 910 for cur_depth in range(reflexion_order): 911 # print(f"(Reflexion Order '{cur_depth}') {ray=}") 912 if should_scale: 913 current_ray_line = [normalize_point(x=cur_target_abs_position[0], y=cur_target_abs_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 914 else: 915 current_ray_line = [(cur_target_abs_position[0], cur_target_abs_position[1])] 916 if should_return_iterative or not remove_iterative: 917 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 918 919 last_abs_position = cur_target_abs_position 920 921 # calculate a target line to update the pixels 922 # target vector 923 dx = math.cos(math.radians(cur_direction_in_degree)) 924 dy = math.sin(math.radians(cur_direction_in_degree)) 925 target_line = [cur_target_abs_position[0], cur_target_abs_position[1], cur_target_abs_position[0], cur_target_abs_position[1]] 926 while (0 <= target_line[2] <= IMG_WIDTH) and (0 <= target_line[3] <= IMG_HEIGHT): 927 target_line[2] += 0.01 * dx 928 target_line[3] += 0.01 * dy 929 930 # update current ray 931 current_position = cur_target_abs_position 932 while True: 933 # update position 934 current_position = update_pixel_position(direction_in_degree=cur_direction_in_degree, cur_position=current_position, target_line=target_line) 935 # for current_position in get_all_pixel_coordinates_in_between(current_position[0], current_position[1], target_line[2], target_line[3]): 936 # last_position_saved = False 937 938 # check if ray is at end 939 if not (0 <= current_position[0] < IMG_WIDTH and 0 <= current_position[1] < IMG_HEIGHT): 940 ray += [current_ray_line] 941 942 if img_border_also_collide: 943 # get reflection angle 944 wall_vector = get_img_border_vector(position=current_position, 945 max_width=IMG_WIDTH, 946 max_height=IMG_HEIGHT) 947 948 # calc new direct vector 949 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 950 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 951 952 # start new beam calculation 953 cur_target_abs_position = last_abs_position 954 cur_direction_in_degree = new_direction_in_degree 955 break 956 else: 957 if should_return_iterative: 958 return ray_iterator 959 return ray 960 961 next_pixel = img[int(current_position[1]), int(current_position[0])] 962 963 # check if hit building 964 if float(next_pixel) in wall_values: 965 last_abs_position = (current_position[0], current_position[1]) 966 ray += [current_ray_line] 967 968 # get building wall reflection angle 969 building_angle = wall_map[int(current_position[1]), int(current_position[0])] 970 if building_angle == np.inf: 971 raise Exception("Got inf value from Wall-Map.") 972 wall_vector = degree_to_vector(building_angle) 973 974 # calc new direct vector 975 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 976 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 977 978 # start new beam calculation 979 cur_target_abs_position = last_abs_position 980 cur_direction_in_degree = new_direction_in_degree 981 break 982 else: 983 # update current ray 984 if should_scale: 985 current_ray_line += [normalize_point(x=current_position[0], y=current_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 986 else: 987 current_ray_line += [(current_position[0], current_position[1])] 988 if should_return_iterative or not remove_iterative: 989 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 990 last_abs_position = (current_position[0], current_position[1]) 991 992 if should_return_iterative: 993 return ray_iterator 994 return ray 995 996 997 998def trace_beam_with_DDA(abs_position, 999 img, 1000 direction_in_degree, 1001 wall_map, 1002 wall_values, 1003 img_border_also_collide=False, 1004 reflexion_order=3, 1005 should_scale=True, 1006 should_return_iterative=False, 1007 remove_iterative=True): 1008 """ 1009 Trace a ray (beam) through a 2D image using a DDA (Digital Differential Analyzer) 1010 algorithm with precise collision points and physically accurate reflections. 1011 1012 The beam starts at a given floating-point position and marches through the grid 1013 until it intersects a wall or exits the image. For each collision, the exact 1014 hit position is computed using the ray Parameters t_hit, ensuring that reflected 1015 segments contain meaningful geometry rather than single-point artifacts. 1016 Reflections are computed using wall normals derived from the `wall_map`. 1017 1018 Parameters: 1019 - abs_position (tuple of float): <br> 1020 Starting position (x, y) of the beam in absolute pixel space. 1021 - img (numpy.ndarray): <br> 1022 2D array representing the scene. Pixel values listed in `wall_values` 1023 are treated as solid walls. 1024 - direction_in_degree (float): <br> 1025 Initial direction of the beam in degrees (0° = right, 90° = down). 1026 - wall_map (numpy.ndarray): <br> 1027 A map storing wall orientations in degrees for each pixel marked as a wall. 1028 These angles define the wall normals used for reflection. 1029 - wall_values (list, tuple, set, float, optional): <br> 1030 Pixel values identifying walls. Any pixel in this list causes a collision. 1031 If None, pixel value 0.0 is treated as a wall. 1032 - img_border_also_collide (bool, optional): <br> 1033 If True, the image borders behave like reflective walls. If False, 1034 the ray terminates when leaving the image. Default: False. 1035 - reflexion_order (int, optional): <br> 1036 Maximum number of reflections. The ray can rebound this many times before 1037 the function terminates. Default: 3. 1038 - should_scale (bool, optional): <br> 1039 If True, all emitted points (x, y) are normalized to [0, 1] range. 1040 Otherwise absolute pixel positions are returned. Default: True. 1041 - should_return_iterative (bool, optional): <br> 1042 Whether to return a RayIterator for step-by-step analysis (default: False). 1043 - remove_iterative (bool, optional): <br> 1044 Whether to optimize (ignore) ray-iterator -> only in use if should_return_iterative is False. 1045 1046 Returns: 1047 - list: 1048 Nested list structure representing the traced ray and its reflections. 1049 Format: ray[beam][point] = (x, y) 1050 """ 1051 reflexion_order += 1 1052 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 1053 1054 ray = [] 1055 if should_return_iterative or not remove_iterative: 1056 ray_iterator = RayIterator() 1057 1058 cur_target_abs_position = (float(abs_position[0]), float(abs_position[1])) 1059 cur_direction_in_degree = direction_in_degree % 360 1060 1061 # Normalize wall_values to set of floats 1062 if wall_values is None: 1063 wall_values_set = {0.0} 1064 elif isinstance(wall_values, (list, tuple, set)): 1065 for idx, v in enumerate(wall_values): 1066 if idx == 0: 1067 wall_values_set = {float(v)} 1068 else: 1069 wall_values_set.add(float(v)) 1070 else: 1071 wall_values_set = {float(wall_values)} 1072 1073 # go through every reflection -> will early stop if hitting a wall (if wall-bouncing is deactivated) 1074 for cur_depth in range(reflexion_order): 1075 if should_scale: 1076 current_ray_line = [normalize_point(x=cur_target_abs_position[0], y=cur_target_abs_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 1077 else: 1078 current_ray_line = [(cur_target_abs_position[0], cur_target_abs_position[1])] 1079 if should_return_iterative or not remove_iterative: 1080 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 1081 1082 last_abs_position = cur_target_abs_position 1083 1084 # direction 1085 rad = math.radians(cur_direction_in_degree) 1086 dx = math.cos(rad) 1087 dy = math.sin(rad) 1088 1089 eps = 1e-12 1090 if abs(dx) < eps: dx = 0.0 1091 if abs(dy) < eps: dy = 0.0 1092 1093 # start float pos and starting cell 1094 x0 = float(cur_target_abs_position[0]) 1095 y0 = float(cur_target_abs_position[1]) 1096 cell_x = int(math.floor(x0)) 1097 cell_y = int(math.floor(y0)) 1098 1099 # outside start -> handle border/reflection/exit 1100 if not (0 <= x0 < IMG_WIDTH and 0 <= y0 < IMG_HEIGHT): 1101 ray += [current_ray_line] 1102 1103 if img_border_also_collide: 1104 wall_vector = get_img_border_vector(position=(x0, y0), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 1105 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1106 cur_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1107 cur_target_abs_position = last_abs_position 1108 continue 1109 else: 1110 if should_return_iterative: 1111 return ray_iterator 1112 return ray 1113 1114 # DDA parameters 1115 tDeltaX = math.inf if dx == 0.0 else abs(1.0 / dx) 1116 tDeltaY = math.inf if dy == 0.0 else abs(1.0 / dy) 1117 1118 if dx > 0: 1119 stepX = 1 1120 nextBoundaryX = cell_x + 1.0 1121 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 1122 elif dx < 0: 1123 stepX = -1 1124 nextBoundaryX = cell_x * 1.0 # left boundary of cell 1125 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 1126 else: 1127 stepX = 0 1128 tMaxX = math.inf 1129 1130 if dy > 0: 1131 stepY = 1 1132 nextBoundaryY = cell_y + 1.0 1133 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 1134 elif dy < 0: 1135 stepY = -1 1136 nextBoundaryY = cell_y * 1.0 1137 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 1138 else: 1139 stepY = 0 1140 tMaxY = math.inf 1141 1142 max_steps = (IMG_WIDTH + IMG_HEIGHT) * 6 1143 steps = 0 1144 last_position_saved = False 1145 1146 # immediate-start-in-wall handling 1147 if 0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT: 1148 start_pixel = float(img[cell_y, cell_x]) 1149 if start_pixel in wall_values_set: 1150 # compute a collision point precisely at start (we'll use origin) 1151 # add collision point (start) and reflect 1152 hit_x = x0 1153 hit_y = y0 1154 if should_scale: 1155 current_ray_line += [normalize_point(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)] 1156 else: 1157 current_ray_line += [(hit_x, hit_y)] 1158 if should_return_iterative or not remove_iterative: 1159 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 1160 ray += [current_ray_line] 1161 1162 building_angle = float(wall_map[cell_y, cell_x]) 1163 if not np.isfinite(building_angle): 1164 raise Exception("Got non-finite value from Wall-Map.") 1165 wall_vector = degree_to_vector(building_angle) 1166 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1167 cur_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1168 ndx, ndy = new_direction[0], new_direction[1] 1169 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 1170 continue 1171 1172 # DDA main loop 1173 while steps < max_steps: 1174 steps += 1 1175 1176 # choose axis to step and capture t_hit (distance along ray to boundary) 1177 if tMaxX < tMaxY: 1178 t_hit = tMaxX 1179 # step in x 1180 cell_x += stepX 1181 tMaxX += tDeltaX 1182 else: 1183 t_hit = tMaxY 1184 # step in y 1185 cell_y += stepY 1186 tMaxY += tDeltaY 1187 1188 # compute exact collision position along ray from origin (x0,y0) 1189 hit_x = x0 + dx * t_hit 1190 hit_y = y0 + dy * t_hit 1191 1192 # For recording the traversal we can append intermediate cell centers encountered so far. 1193 # But more importantly, append the collision point to the current segment BEFORE storing it. 1194 if should_scale: 1195 current_ray_line += [normalize_point(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)] 1196 else: 1197 current_ray_line += [(hit_x, hit_y)] 1198 if should_return_iterative or not remove_iterative: 1199 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 1200 1201 # Now check if we've left the image bounds (cell_x, cell_y refer to the new cell we stepped into) 1202 if not (0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT): 1203 ray += [current_ray_line] 1204 last_position_saved = True 1205 1206 if img_border_also_collide: 1207 wall_vector = get_img_border_vector(position=(cell_x, cell_y), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 1208 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1209 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1210 # start next ray from last in-image position (hit_x, hit_y) nudged slightly 1211 ndx, ndy = new_direction[0], new_direction[1] 1212 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 1213 cur_direction_in_degree = new_direction_in_degree 1214 break 1215 else: 1216 if should_return_iterative: 1217 return ray_iterator 1218 return ray 1219 1220 # sample the pixel in the cell we stepped into 1221 next_pixel = float(img[cell_y, cell_x]) 1222 if next_pixel in wall_values_set: 1223 # we hit a wall — collision point already appended 1224 last_abs_position = (hit_x, hit_y) 1225 ray += [current_ray_line] 1226 last_position_saved = True 1227 1228 building_angle = float(wall_map[cell_y, cell_x]) 1229 if not np.isfinite(building_angle): 1230 raise Exception("Got non-finite value from Wall-Map.") 1231 wall_vector = degree_to_vector(building_angle) 1232 1233 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1234 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1235 1236 # start next beam from collision point nudged outwards 1237 ndx, ndy = new_direction[0], new_direction[1] 1238 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 1239 cur_direction_in_degree = new_direction_in_degree 1240 break 1241 else: 1242 # no hit -> continue marching; also add a representative point in the traversed cell (optional) 1243 # we already appended the exact hit point for this step; for smoother lines you may add cell center too 1244 last_abs_position = (hit_x, hit_y) 1245 # continue 1246 1247 # end DDA loop 1248 if not last_position_saved: 1249 ray.append(current_ray_line) 1250 1251 if should_return_iterative: 1252 return ray_iterator 1253 return ray 1254 1255 1256 1257def trace_beams(rel_position, 1258 img_src, 1259 directions_in_degree, 1260 wall_values, 1261 wall_thickness=0, 1262 img_border_also_collide=False, 1263 reflexion_order=3, 1264 should_scale_rays=True, 1265 should_scale_img=True, 1266 use_dda=True, 1267 iterative_tracking=False, 1268 iterative_steps=None, 1269 parallelization=-1, 1270 parallelization_method="processes", # "threads", "processes" 1271 use_numba_compilation=True, 1272 ignore_iterative_optimization=True): 1273 """ 1274 Trace multiple rays (beams) from a single position through an image with walls and reflections. 1275 1276 Each beam starts from a given relative position and follows its assigned direction 1277 until it collides with a wall or image border. On collisions, reflections are 1278 computed based on local wall normals extracted from the image. 1279 1280 Parameters: 1281 - rel_position (tuple): <br> 1282 Relative starting position (x, y) in normalized coordinates [0-1]. 1283 - img_src (str or numpy.ndarray): <br> 1284 Input image (array or file path) used for wall detection. 1285 - directions_in_degree (list): <br> 1286 List of beam direction angles (in degrees). 1287 - wall_values (list or float or None): <br> 1288 Pixel values representing walls or obstacles. 1289 - wall_thickness (int, optional): <br> 1290 Thickness (in pixels) of detected walls (default: 0). 1291 - img_border_also_collide (bool, optional): <br> 1292 Whether image borders act as colliders (default: False). 1293 - reflexion_order (int, optional): <br> 1294 Number of allowed reflections per beam (default: 3). 1295 - should_scale_rays (bool, optional): <br> 1296 Whether to normalize ray coordinates to [0, 1] (default: True). 1297 - should_scale_img (bool, optional): <br> 1298 Whether to scale the input image before wall detection (default: True). 1299 - use_dda (bool, optional): <br> 1300 Whether to use the DDA-based ray tracing method (default: True). 1301 - iterative_tracking (bool, optional): 1302 Whether to return a RayIterator for step-by-step analysis (default: False). 1303 - iterative_steps (int, optional):<br> 1304 Number of steps for iterative reduction if using iterative tracking. `None` for all steps. 1305 - parallelization (int, optional):<br> 1306 The amount of workers for parallelization. 0 for no parallelization, -1 for max amount of workers. 1307 - parallelization_method (str, optional):<br> 1308 Method to use for parallelization (as soft condition) -> "threads" or "processes" 1309 - use_numba_compilation (bool, optional):<br> 1310 Whether to use the compiled (to machine code) version of compute heavy functions. 1311 - ignore_iterative_optimization (bool, optional): <br> 1312 Whether to used optimized ignore iteration data if iterative_tracking is False. 1313 1314 Returns: 1315 - list: <br> 1316 Nested list of traced beams and their reflection segments. 1317 Format: rays[beam][segment][point] = (x, y) 1318 """ 1319 if use_numba_compilation and iterative_tracking: 1320 print("[WARNING] Iterative Return deactivates the use of numba.") 1321 use_numba_compilation = False 1322 1323 if isinstance(img_src, np.ndarray): 1324 img = img_src 1325 else: 1326 img = img_open(src=img_src, should_scale=should_scale_img, should_print=False) 1327 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 1328 abs_position = (rel_position[0] * IMG_WIDTH, rel_position[1] * IMG_HEIGHT) 1329 1330 if wall_values is not None and type(wall_values) not in [list, tuple]: 1331 wall_values = [wall_values] 1332 wall_map = get_wall_map(img=img, 1333 wall_values=wall_values, 1334 thickness=wall_thickness, 1335 use_numba_compilation=use_numba_compilation) 1336 1337 if wall_values is None: 1338 wall_values = [0.0] 1339 1340 # create function plan 1341 ray_planning = [] 1342 1343 for direction_in_degree in directions_in_degree: 1344 if use_dda: 1345 if use_numba_compilation: 1346 ray_tracing_func = trace_beam_with_DDA_numba 1347 else: 1348 ray_tracing_func = trace_beam_with_DDA 1349 else: 1350 if use_numba_compilation: 1351 ray_tracing_func = trace_beam_numba 1352 else: 1353 ray_tracing_func = trace_beam 1354 1355 ray_planning.append(lambda dir=direction_in_degree: ray_tracing_func( 1356 abs_position=abs_position, 1357 img=img, 1358 direction_in_degree=dir, 1359 wall_map=wall_map, 1360 wall_values=np.array(wall_values, dtype=np.float64) if (use_numba_compilation and use_dda) else wall_values, 1361 img_border_also_collide=img_border_also_collide, 1362 reflexion_order=reflexion_order, 1363 should_scale=should_scale_rays, 1364 should_return_iterative=iterative_tracking, 1365 remove_iterative=ignore_iterative_optimization 1366 ) 1367 ) 1368 1369 if iterative_tracking: 1370 rays = RayIterator() 1371 else: 1372 rays = [] 1373 1374 # compute 1375 if parallelization == 0: 1376 1377 for cur_ray_planning in ray_planning: 1378 cur_ray_result = cur_ray_planning() 1379 if use_numba_compilation: 1380 cur_ray_result = numba_to_py(cur_ray_result) 1381 1382 if iterative_tracking: 1383 rays.add_rays(cur_ray_result) 1384 else: 1385 rays.append(cur_ray_result) 1386 else: 1387 result_rays = Parallel(n_jobs=parallelization, 1388 # backend="threading" if use_numba_compilation else "loky", # process-based 1389 prefer="threads" if use_numba_compilation else parallelization_method, 1390 return_as="generator", 1391 batch_size=1 # because of unequal ray lengths 1392 )( 1393 delayed(ray_func)() for ray_func in ray_planning 1394 ) 1395 1396 for cur_ray_result in result_rays: 1397 if use_numba_compilation: 1398 cur_ray_result = numba_to_py(cur_ray_result) 1399 1400 if iterative_tracking: 1401 rays.add_rays(cur_ray_result) 1402 else: 1403 rays.append(cur_ray_result) 1404 1405 if iterative_tracking and iterative_steps is not None: 1406 rays.reduce_rays_iteratively(steps=iterative_steps) 1407 1408 return rays 1409 1410 1411 1412def scale_rays(rays, 1413 max_x=None, max_y=None, 1414 new_max_x=None, new_max_y=None, 1415 detailed_scaling=True): 1416 """ 1417 Scale ray coordinates between coordinate systems or image resolutions. 1418 1419 Optionally normalizes rays by old dimensions and rescales them to new ones. 1420 Can scale all points or just the start/end points of each beam. 1421 1422 Parameters: 1423 - rays (list): <br> 1424 Nested list of rays in the format rays[ray][beam][point] = (x, y). 1425 - max_x (float, optional): <br> 1426 Original maximum x-value for normalization. 1427 - max_y (float, optional): <br> 1428 Original maximum y-value for normalization. 1429 - new_max_x (float, optional): <br> 1430 New maximum x-value after rescaling. 1431 - new_max_y (float, optional): <br> 1432 New maximum y-value after rescaling. 1433 - detailed_scaling (bool, optional): <br> 1434 If True, scale every point in a beam; otherwise, only endpoints (default: True). 1435 1436 Returns: 1437 - list: <br> 1438 Scaled rays in the same nested format. 1439 """ 1440 if isinstance(rays, RayIterator): 1441 rays.apply_and_update(lambda r: scale_rays(r, max_x=max_x, max_y=max_y, new_max_x=new_max_x, new_max_y=new_max_y, detailed_scaling=detailed_scaling)) 1442 return rays 1443 1444 scaled_rays = [] 1445 for ray in rays: 1446 scaled_ray = [] 1447 for beams in ray: 1448 new_beams = copy.deepcopy(beams) 1449 if detailed_scaling: 1450 idx_to_process = list(range(len(beams))) 1451 else: 1452 idx_to_process = [0, len(beams)-1] 1453 1454 for idx in idx_to_process: 1455 x1, y1 = beams[idx] 1456 1457 if max_x is not None and max_y is not None: 1458 x1 /= max_x 1459 y1 /= max_y 1460 1461 from_cache = (x1, y1) 1462 if new_max_x is not None and new_max_y is not None: 1463 if x1 >= new_max_x/2: 1464 print(f"[WARNING] Detected high values scaling. Are you sure you want to scale for example a ray with {x1} to a value like {x1*new_max_x}?") 1465 if y1 >= new_max_y/2: 1466 print(f"[WARNING] Detected high values scaling. Are you sure you want to scale for example a ray with {y1} to a value like {y1*new_max_y}?") 1467 1468 x1 *= new_max_x 1469 y1 *= new_max_y 1470 1471 new_beams[idx] = (x1, y1) 1472 1473 scaled_ray.append(new_beams) 1474 scaled_rays.append(scaled_ray) 1475 return scaled_rays 1476 1477 1478 1479def draw_rectangle_with_thickness(img, start_point, end_point, value, thickness=1): 1480 """ 1481 Draw a filled or thick rectangle on an image. 1482 1483 Expands the given start and end points based on the desired thickness and 1484 clips coordinates to image bounds to avoid overflow. 1485 1486 Parameters: 1487 - img (numpy.ndarray): <br> 1488 Image array to draw on. 1489 - start_point (tuple): <br> 1490 Top-left corner of the rectangle (x, y). 1491 - end_point (tuple): <br> 1492 Bottom-right corner of the rectangle (x, y). 1493 - value (int or float): <br> 1494 Fill value or color intensity. 1495 - thickness (int, optional): <br> 1496 Rectangle border thickness; 1497 if <= 0, the rectangle is filled (default: 1). 1498 """ 1499 # Calculate the expansion -> "thickness" 1500 if thickness > 0: 1501 expand = thickness // 2 1502 x1, y1 = start_point[0] - expand, start_point[1] - expand 1503 x2, y2 = end_point[0] + expand, end_point[1] + expand 1504 else: 1505 # thickness <= 0 → filled rectangle, no expansion 1506 x1, y1 = start_point 1507 x2, y2 = end_point 1508 1509 # Clip coordinates to image bounds 1510 x1 = max(0, x1) 1511 y1 = max(0, y1) 1512 x2 = min(img.shape[1]-1, x2) 1513 y2 = min(img.shape[0]-1, y2) 1514 1515 cv2.rectangle(img, (x1, y1), (x2, y2), value, thickness=-1) 1516 1517 1518 1519def draw_line_or_point(img, start_point, end_point, fill_value, thickness): 1520 """ 1521 Draw either a line or a single point on an image. 1522 1523 Determines whether to draw a point or a line based on whether the start and 1524 end coordinates are identical. 1525 1526 Parameters: 1527 - img (numpy.ndarray): <br> 1528 Image array to draw on. 1529 - start_point (tuple): <br> 1530 Starting pixel coordinate (x, y). 1531 - end_point (tuple): <br> 1532 Ending pixel coordinate (x, y). 1533 - fill_value (int or float): <br> 1534 Value or color used for drawing. 1535 - thickness (int): <br> 1536 Thickness of the line or point. 1537 """ 1538 draw_point = (start_point == end_point) 1539 1540 if draw_point: 1541 draw_rectangle_with_thickness(img=img, start_point=start_point, end_point=end_point, value=fill_value, thickness=thickness) 1542 else: 1543 cv2.line(img, start_point, end_point, fill_value, thickness) 1544 1545 1546 1547def draw_rays(rays, detail_draw=True, 1548 output_format="single_image", # single_image, multiple_images, channels 1549 img_background=None, ray_value=255, ray_thickness=1, 1550 img_shape=(256, 256), dtype=float, standard_value=0, 1551 should_scale_rays_to_image=True, original_max_width=None, original_max_height=None, 1552 show_only_reflections=False): 1553 """ 1554 Render rays onto an image or a set of images. 1555 1556 Each ray can be drawn in full detail (every point) or as straight lines between 1557 beam endpoints. Rays can be scaled to match image dimensions and drawn on a 1558 single image, multiple images, or separate channels. 1559 1560 Parameters: 1561 - rays (list): <br> 1562 Nested list of rays in the format rays[ray][beam][point] = (x, y). 1563 - detail_draw (bool, optional): <br> 1564 Whether to draw every point or just beam endpoints (default: True). 1565 - output_format (str, optional): <br> 1566 Output mode: "single_image", "multiple_images", or "channels" (default: "single_image"). 1567 - img_background (numpy.ndarray, optional): <br> 1568 Background image; if None, a blank image is created. 1569 - ray_value (int, float, list, or numpy.ndarray, optional): <br> 1570 Pixel intensity or color per reflection (default: 255). 1571 - ray_thickness (int, optional): <br> 1572 Thickness of the drawn lines or points (default: 1). 1573 - img_shape (tuple, optional): <br> 1574 Shape of the generated image if no background is given (default: (256, 256)). 1575 - dtype (type, optional): <br> 1576 Data type for the output image (default: float). 1577 - standard_value (int or float, optional): <br> 1578 Background fill value (default: 0). 1579 - should_scale_rays_to_image (bool, optional): <br> 1580 Whether to scale ray coordinates to match the image (default: True). 1581 - original_max_width (float, optional): <br> 1582 Original image width before scaling. 1583 - original_max_height (float, optional): <br> 1584 Original image height before scaling. 1585 - show_only_reflections (bool, optional): <br> 1586 If True, draws only reflected beams (default: False). 1587 1588 Returns: 1589 numpy.ndarray or list: <br> 1590 - Single image if output_format == "single_image" or "channels". 1591 - List of images if output_format == "multiple_images". 1592 """ 1593 if isinstance(rays, RayIterator): 1594 imgs = rays.apply_and_return(lambda r: draw_rays(r, detail_draw=detail_draw, 1595 output_format=output_format, 1596 img_background=img_background, 1597 ray_value=ray_value, 1598 ray_thickness=ray_thickness, 1599 img_shape=img_shape, 1600 dtype=dtype, 1601 standard_value=standard_value, 1602 should_scale_rays_to_image=should_scale_rays_to_image, 1603 original_max_width=original_max_width, 1604 original_max_height=original_max_height, 1605 show_only_reflections=show_only_reflections)) 1606 return imgs 1607 1608 # prepare background image 1609 if img_background is None: 1610 img = np.full(shape=img_shape, fill_value=dtype(standard_value), dtype=dtype) 1611 else: 1612 img = img_background.copy() 1613 1614 # rescale rays to fit inside image bounds if desired 1615 height, width = img.shape[:2] 1616 # print(f"{height, width=}") 1617 # print(f"{(original_max_width, original_max_height)=}") 1618 if should_scale_rays_to_image: 1619 rays = scale_rays(rays, max_x=original_max_width, max_y=original_max_height, new_max_x=width-1, new_max_y=height-1, detailed_scaling=detail_draw) 1620 1621 nrays = len(rays) 1622 if output_format == "channels": 1623 img = np.repeat(img[..., None], nrays, axis=-1) 1624 # img_shape += (nrays, ) 1625 # img = np.full(shape=img_shape, fill_value=dtype(standard_value), dtype=dtype) 1626 elif output_format == "multiple_images": 1627 imgs = [np.copy(img) for _ in range(nrays)] 1628 1629 # draw on image 1630 for idx, ray in enumerate(rays): 1631 for reflexion_order, beam_points in enumerate(ray): 1632 1633 if detail_draw: 1634 lines = [] 1635 for cur_point in range(0, len(beam_points)): 1636 start_point = tuple(map(lambda x:int(x), beam_points[cur_point])) 1637 end_point = tuple(map(lambda x:int(x), beam_points[cur_point])) 1638 # end_point = tuple(map(lambda x:int(x), beam_points[cur_point+1])) 1639 # -> if as small lines then in range: range(0, len(beam_points)-1) 1640 lines.append((start_point, end_point, reflexion_order)) 1641 else: 1642 start_point = tuple(map(lambda x:int(x), beam_points[0])) 1643 end_point = tuple(map(lambda x:int(x), beam_points[-1])) 1644 lines = [(start_point, end_point, reflexion_order)] 1645 1646 for start_point, end_point, reflexion_order in lines: 1647 1648 if show_only_reflections and reflexion_order == 0: 1649 continue 1650 1651 # get cur ray value 1652 if type(ray_value) in [list, tuple, np.ndarray]: 1653 # if we print without the first line: first value (index 0) belkongs to the reflexion order 1 1654 cur_reflexion_index = reflexion_order-1 if show_only_reflections else reflexion_order 1655 cur_ray_value = ray_value[min(len(ray_value)-1, cur_reflexion_index)] 1656 else: 1657 cur_ray_value = ray_value 1658 1659 if output_format == "channels": 1660 layer = np.ascontiguousarray(img[..., idx]) 1661 draw_line_or_point(img=layer, start_point=start_point, end_point=end_point, fill_value=cur_ray_value, thickness=ray_thickness) 1662 img[..., idx] = layer 1663 elif output_format == "multiple_images": 1664 draw_line_or_point(img=imgs[idx], start_point=start_point, end_point=end_point, fill_value=cur_ray_value, thickness=ray_thickness) 1665 else: 1666 draw_line_or_point(img=img, start_point=start_point, end_point=end_point, fill_value=cur_ray_value, thickness=ray_thickness) 1667 1668 1669 if output_format == "multiple_images": 1670 return imgs 1671 else: 1672 return img 1673 1674 1675 1676def trace_and_draw_rays(rel_position, 1677 img_src, 1678 directions_in_degree, 1679 wall_values, 1680 wall_thickness=0, 1681 img_border_also_collide=False, 1682 reflexion_order=3, 1683 should_scale_rays=True, 1684 should_scale_img=True, 1685 use_dda=True, 1686 iterative_tracking=False, 1687 iterative_steps=None, 1688 parallelization=-1, 1689 parallelization_method="processes", # "threads", "processes" 1690 use_numba_compilation=True, 1691 ignore_iterative_optimization=True, 1692 detail_draw=True, 1693 output_format="single_image", # single_image, multiple_images, channels 1694 img_background=None, 1695 ray_value=255, 1696 ray_thickness=1, 1697 img_shape=(256, 256), 1698 dtype=float, 1699 standard_value=0, 1700 should_scale_rays_to_image=True, 1701 original_max_width=None, 1702 original_max_height=None, 1703 show_only_reflections=False): 1704 """ 1705 Trace multiple rays (beams) from a single position through an image with walls and reflections AND 1706 render rays onto an image or a set of images. 1707 1708 Calls internally `trace_beams` and then `draw_rays`. 1709 1710 Parameters: 1711 - rel_position (tuple): <br> 1712 Relative starting position (x, y) in normalized coordinates [0-1]. 1713 - img_src (str or numpy.ndarray): <br> 1714 Input image (array or file path) used for wall detection. 1715 - directions_in_degree (list): <br> 1716 List of beam direction angles (in degrees). 1717 - wall_values (list or float or None): <br> 1718 Pixel values representing walls or obstacles. 1719 - wall_thickness (int, optional): <br> 1720 Thickness (in pixels) of detected walls (default: 0). 1721 - img_border_also_collide (bool, optional): <br> 1722 Whether image borders act as colliders (default: False). 1723 - reflexion_order (int, optional): <br> 1724 Number of allowed reflections per beam (default: 3). 1725 - should_scale_rays (bool, optional): <br> 1726 Whether to normalize ray coordinates to [0, 1] (default: True). 1727 - should_scale_img (bool, optional): <br> 1728 Whether to scale the input image before wall detection (default: True). 1729 - use_dda (bool, optional): <br> 1730 Whether to use the DDA-based ray tracing method (default: True). 1731 - iterative_tracking (bool, optional): 1732 Whether to return a RayIterator for step-by-step analysis (default: False). 1733 - iterative_steps (int, optional):<br> 1734 Number of steps for iterative reduction if using iterative tracking. `None` for all steps. 1735 - parallelization (int, optional):<br> 1736 The amount of workers for parallelization. 0 for no parallelization, -1 for max amount of workers. 1737 - parallelization_method (str, optional):<br> 1738 Method to use for parallelization (as soft condition) -> "threads" or "processes" 1739 - use_numba_compilation (bool, optional):<br> 1740 Whether to use the compiled (to machine code) version of compute heavy functions. 1741 - ignore_iterative_optimization (bool, optional): <br> 1742 Whether to used optimized ignore iteration data if iterative_tracking is False. 1743 - detail_draw (bool, optional): <br> 1744 Whether to draw every point or just beam endpoints (default: True). 1745 - output_format (str, optional): <br> 1746 Output mode: "single_image", "multiple_images", or "channels" (default: "single_image"). 1747 - img_background (numpy.ndarray, optional): <br> 1748 Background image; if None, a blank image is created. 1749 - ray_value (int, float, list, or numpy.ndarray, optional): <br> 1750 Pixel intensity or color per reflection (default: 255). 1751 - ray_thickness (int, optional): <br> 1752 Thickness of the drawn lines or points (default: 1). 1753 - img_shape (tuple, optional): <br> 1754 Shape of the generated image if no background is given (default: (256, 256)). 1755 - dtype (type, optional): <br> 1756 Data type for the output image (default: float). 1757 - standard_value (int or float, optional): <br> 1758 Background fill value (default: 0). 1759 - should_scale_rays_to_image (bool, optional): <br> 1760 Whether to scale ray coordinates to match the image (default: True). 1761 - original_max_width (float, optional): <br> 1762 Original image width before scaling. 1763 - original_max_height (float, optional): <br> 1764 Original image height before scaling. 1765 - show_only_reflections (bool, optional): <br> 1766 If True, draws only reflected beams (default: False). 1767 1768 Returns: 1769 numpy.ndarray or list: 1770 - Single image if output_format == "single_image" or "channels". 1771 - List of images if output_format == "multiple_images". 1772 """ 1773 rays = trace_beams(rel_position, 1774 img_src, 1775 directions_in_degree, 1776 wall_values, 1777 wall_thickness=wall_thickness, 1778 img_border_also_collide=img_border_also_collide, 1779 reflexion_order=reflexion_order, 1780 should_scale_rays=should_scale_rays, 1781 should_scale_img=should_scale_img, 1782 use_dda=use_dda, 1783 iterative_tracking=iterative_tracking, 1784 iterative_steps=iterative_steps, 1785 parallelization=parallelization, 1786 parallelization_method=parallelization_method, 1787 use_numba_compilation=use_numba_compilation, 1788 ignore_iterative_optimization=ignore_iterative_optimization) 1789 1790 return draw_rays(rays, 1791 detail_draw=detail_draw, 1792 output_format=output_format, 1793 img_background=img_background, 1794 ray_value=ray_value, 1795 ray_thickness=ray_thickness, 1796 img_shape=img_shape, 1797 dtype=dtype, 1798 standard_value=standard_value, 1799 should_scale_rays_to_image=should_scale_rays_to_image, 1800 original_max_width=original_max_width, 1801 original_max_height=original_max_height, 1802 show_only_reflections=show_only_reflections) 1803 1804 1805 1806# -------------------------------- 1807# >>> Numba Optimized Versions <<< 1808# -------------------------------- 1809def numba_to_py(obj): 1810 """ 1811 Converts numba.typed.List (possibly nested -> rays) to plain Python lists/tuples. 1812 """ 1813 if isinstance(obj, List): 1814 return [numba_to_py(x) for x in obj] 1815 1816 1817 if isinstance(obj, tuple): 1818 return tuple(numba_to_py(x) for x in obj) 1819 1820 return obj 1821 1822 1823TYPE_POINT_INT = types.UniTuple(types.int64, 2) 1824TYPE_POINT_FLOAT = types.UniTuple(types.float64, 2) 1825TYPE_LINE = types.ListType(TYPE_POINT_FLOAT) 1826 1827# trace_beam_numba = numba.njit(cache=True, fastmath=True)(trace_beam) 1828# trace_beam_with_DDA_numba = numba.njit(cache=True, fastmath=True)(trace_beam_with_DDA) 1829calc_reflection_numba = numba.njit(cache=True, fastmath=True)(calc_reflection) 1830# get_all_pixel_coordinates_in_between_numba = numba.njit(cache=True, fastmath=True)(get_all_pixel_coordinates_in_between) 1831get_img_border_vector_numba = numba.njit(cache=True, fastmath=True)(get_img_border_vector) 1832# update_pixel_position_numba = numba.njit(cache=True, fastmath=True)(update_pixel_position) 1833 1834 1835 1836@numba.njit(cache=True, fastmath=True) 1837def update_pixel_position_numba(direction_in_degree, cur_position, target_line): 1838 # cur_position: (x,y) tuple 1839 # target_line: array[4] => [x1,y1,x2,y2] 1840 px = cur_position[0] 1841 py = cur_position[1] 1842 1843 ax = target_line[0] 1844 ay = target_line[1] 1845 bx = target_line[2] 1846 by = target_line[3] 1847 1848 # Directionvector of the line 1849 vx = bx - ax 1850 vy = by - ay 1851 1852 # Projection: t = dot(P-A, V) / dot(V,V) 1853 wx = px - ax 1854 wy = py - ay 1855 1856 denom = vx*vx + vy*vy 1857 if denom == 0.0: 1858 # Degenerated Linie: just no movement 1859 return (px, py) 1860 1861 t = (wx*vx + wy*vy) / denom 1862 1863 # clip scalar (Numba-safe) 1864 if t < 0.0: 1865 t = 0.0 1866 elif t > 1.0: 1867 t = 1.0 1868 1869 # Point on the line 1870 lx = ax + t * vx 1871 ly = ay + t * vy 1872 1873 # Now make a step in direction_in_degree 1874 rad = math.radians(direction_in_degree) 1875 dx = math.cos(rad) 1876 dy = math.sin(rad) 1877 1878 nx = lx + dx 1879 ny = ly + dy 1880 1881 return (nx, ny) 1882 1883 1884 1885@numba.njit(cache=True, fastmath=True) 1886def _is_in_wall_values(pixel_value, wall_values): 1887 # Numba-save Membership-Check -> works with np.ndarray or numba List 1888 for i in range(len(wall_values)): 1889 if pixel_value == wall_values[i]: 1890 return True 1891 return False 1892 1893 1894 1895@numba.njit(cache=True, fastmath=True) 1896def _compute_target_line_outside(x0, y0, dx, dy, w, h): 1897 # w, h als float 1898 # Candidats: Intersection with vertical/horizontal borders 1899 # Taking the smallest t and continue going 0.01 1900 INF = 1e30 1901 1902 # X-Border 1903 if dx > 0.0: 1904 tx = (w - x0) / dx 1905 elif dx < 0.0: 1906 tx = (0.0 - x0) / dx 1907 else: 1908 tx = INF 1909 1910 # Y-Border 1911 if dy > 0.0: 1912 ty = (h - y0) / dy 1913 elif dy < 0.0: 1914 ty = (0.0 - y0) / dy 1915 else: 1916 ty = INF 1917 1918 # t have to be positive 1919 if tx <= 0.0: 1920 tx = INF 1921 if ty <= 0.0: 1922 ty = INF 1923 1924 t_hit = tx if tx < ty else ty 1925 if t_hit == INF: 1926 # Direction is 0-vector (should not happen) 1927 t_hit = 0.0 1928 1929 # the end point should be stay outside of the building/wall 1930 t_out = t_hit + 0.01 1931 1932 x2 = x0 + t_out * dx 1933 y2 = y0 + t_out * dy 1934 1935 return x0, y0, x2, y2 1936 1937 1938 1939@numba.njit(cache=True, fastmath=True) 1940def trace_beam_numba(abs_position, 1941 img, 1942 direction_in_degree, 1943 wall_map, 1944 wall_values, 1945 img_border_also_collide=False, 1946 reflexion_order=3, 1947 should_scale=True, 1948 should_return_iterative=False, 1949 remove_iterative=True): 1950 """ 1951 Trace a ray (beam) through an image with walls and reflections. 1952 1953 The beam starts from a given position and follows a direction until it hits 1954 a wall or border. On collisions, reflections are computed using wall normals. 1955 1956 Parameters: 1957 - abs_position (tuple): <br> 1958 Starting position (x, y) of the beam. 1959 - img (numpy.ndarray): <br> 1960 Input image or segmentation map. 1961 - direction_in_degree (float): <br> 1962 Initial direction angle of the beam. 1963 - wall_map (numpy.ndarray): <br> 1964 Map containing wall orientations in degrees. 1965 - wall_values (list): <br> 1966 List of pixel values representing walls. 1967 - img_border_also_collide (bool, optional): <br> 1968 Whether the image border acts as a collider (default: False). 1969 - reflexion_order (int, optional): <br> 1970 Number of allowed reflections (default: 3). 1971 - should_scale (bool, optional): <br> 1972 Whether to normalize positions to [0, 1] (default: True). 1973 - should_return_iterative (bool, optional): <br> 1974 Whether to return a RayIterator for step-by-step analysis (default: False). 1975 - ignore_iterative_optimization (bool, optional): <br> 1976 Whether to used optimized ignore iteration data if iterative_tracking is False. 1977 - remove_iterative (bool, optional): <br> 1978 Ignored in this numba version. 1979 1980 Returns: 1981 - list: 1982 Nested list structure representing the traced ray and its reflections. 1983 Format: ray[beam][point] = (x, y) 1984 """ 1985 if should_return_iterative: 1986 print("[WARNING] Numba Version can't return a iterative version.") 1987 1988 reflexion_order += 1 1989 IMG_HEIGHT = img.shape[0] 1990 IMG_WIDTH = img.shape[1] 1991 1992 ray = List.empty_list(TYPE_LINE) 1993 1994 cur_target_abs_position = (float(abs_position[0]), float(abs_position[1])) 1995 cur_direction_in_degree = float(direction_in_degree % 360.0) 1996 1997 w_f = float(IMG_WIDTH) 1998 h_f = float(IMG_HEIGHT) 1999 2000 inf_val = np.inf 2001 2002 for _ in range(reflexion_order): 2003 # current_ray_line als typed list 2004 current_ray_line = List.empty_list(TYPE_POINT_FLOAT) 2005 2006 if should_scale: 2007 p = normalize_point_numba(x=cur_target_abs_position[0], 2008 y=cur_target_abs_position[1], 2009 width=IMG_WIDTH, 2010 height=IMG_HEIGHT) 2011 current_ray_line.append((float(p[0]), float(p[1]))) 2012 else: 2013 current_ray_line.append((cur_target_abs_position[0], cur_target_abs_position[1])) 2014 2015 last_abs_position = cur_target_abs_position 2016 2017 # Direction -> dx,dy 2018 rad = math.radians(cur_direction_in_degree) 2019 dx = math.cos(rad) 2020 dy = math.sin(rad) 2021 2022 # target_line 2023 x1, y1, x2, y2 = _compute_target_line_outside(cur_target_abs_position[0], 2024 cur_target_abs_position[1], 2025 dx, dy, w_f, h_f) 2026 # build target line -> start and end point 2027 target_line = np.empty(4, dtype=np.float64) 2028 target_line[0] = x1 2029 target_line[1] = y1 2030 target_line[2] = x2 2031 target_line[3] = y2 2032 2033 current_position = cur_target_abs_position 2034 2035 while True: 2036 current_position = update_pixel_position_numba( 2037 direction_in_degree=cur_direction_in_degree, 2038 cur_position=current_position, 2039 target_line=target_line 2040 ) 2041 2042 x = current_position[0] 2043 y = current_position[1] 2044 2045 # Border check 2046 if not (0.0 <= x < w_f and 0.0 <= y < h_f): 2047 ray.append(current_ray_line) 2048 2049 if img_border_also_collide: 2050 wall_vector = get_img_border_vector_numba( 2051 position=current_position, 2052 max_width=IMG_WIDTH, 2053 max_height=IMG_HEIGHT 2054 ) 2055 new_direction = calc_reflection_numba( 2056 collide_vector=degree_to_vector_numba(cur_direction_in_degree), 2057 wall_vector=wall_vector 2058 ) 2059 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2060 2061 cur_target_abs_position = last_abs_position 2062 cur_direction_in_degree = float(new_direction_in_degree) 2063 break 2064 else: 2065 return ray 2066 2067 ix = int(x) 2068 iy = int(y) 2069 2070 next_pixel = float(img[iy, ix]) 2071 2072 # Wall check (Numba-save) 2073 if _is_in_wall_values(next_pixel, wall_values): 2074 last_abs_position = (x, y) 2075 ray.append(current_ray_line) 2076 2077 building_angle = wall_map[iy, ix] 2078 if building_angle == inf_val: 2079 raise Exception("Got inf value from Wall-Map.") 2080 2081 wall_vector = degree_to_vector_numba(building_angle) 2082 2083 new_direction = calc_reflection_numba( 2084 collide_vector=degree_to_vector_numba(cur_direction_in_degree), 2085 wall_vector=wall_vector 2086 ) 2087 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2088 2089 cur_target_abs_position = last_abs_position 2090 cur_direction_in_degree = float(new_direction_in_degree) 2091 break 2092 else: 2093 if should_scale: 2094 p = normalize_point_numba(x=x, y=y, width=IMG_WIDTH, height=IMG_HEIGHT) 2095 current_ray_line.append((float(p[0]), float(p[1]))) 2096 else: 2097 current_ray_line.append((x, y)) 2098 last_abs_position = (x, y) 2099 2100 return ray 2101 2102 2103 2104@numba.njit(cache=True, fastmath=True) 2105def trace_beam_with_DDA_numba(abs_position, 2106 img, 2107 direction_in_degree, 2108 wall_map, 2109 wall_values, 2110 img_border_also_collide=False, 2111 reflexion_order=3, 2112 should_scale=True, 2113 should_return_iterative=False, 2114 remove_iterative=True): 2115 """ 2116 Trace a ray (beam) through a 2D image using a DDA (Digital Differential Analyzer) 2117 algorithm with precise collision points and physically accurate reflections. 2118 2119 The beam starts at a given floating-point position and marches through the grid 2120 until it intersects a wall or exits the image. For each collision, the exact 2121 hit position is computed using the ray Parameters t_hit, ensuring that reflected 2122 segments contain meaningful geometry rather than single-point artifacts. 2123 Reflections are computed using wall normals derived from the `wall_map`. 2124 2125 Parameters: 2126 - abs_position (tuple of float): <br> 2127 Starting position (x, y) of the beam in absolute pixel space. 2128 - img (numpy.ndarray): <br> 2129 2D array representing the scene. Pixel values listed in `wall_values` 2130 are treated as solid walls. 2131 - direction_in_degree (float): <br> 2132 Initial direction of the beam in degrees (0° = right, 90° = down). 2133 - wall_map (numpy.ndarray): <br> 2134 A map storing wall orientations in degrees for each pixel marked as a wall. 2135 These angles define the wall normals used for reflection. 2136 - wall_values (list, tuple, set, float, optional): <br> 2137 Pixel values identifying walls. Any pixel in this list causes a collision. 2138 If None, pixel value 0.0 is treated as a wall. 2139 - img_border_also_collide (bool, optional): <br> 2140 If True, the image borders behave like reflective walls. If False, 2141 the ray terminates when leaving the image. Default: False. 2142 - reflexion_order (int, optional): <br> 2143 Maximum number of reflections. The ray can rebound this many times before 2144 the function terminates. Default: 3. 2145 - should_scale (bool, optional): <br> 2146 If True, all emitted points (x, y) are normalized to [0, 1] range. 2147 Otherwise absolute pixel positions are returned. Default: True. 2148 - should_return_iterative (bool, optional): <br> 2149 Whether to return a RayIterator for step-by-step analysis (default: False). 2150 - remove_iterative (bool, optional): <br> 2151 Ignored in this numba version. 2152 2153 Returns: 2154 - list: 2155 Nested list structure representing the traced ray and its reflections. 2156 Format: ray[beam][point] = (x, y) 2157 """ 2158 if should_return_iterative: 2159 print("[WARNING] Numba Version can't return a iterative version.") 2160 2161 reflexion_order += 1 2162 # IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 2163 IMG_HEIGHT = img.shape[0] 2164 IMG_WIDTH = img.shape[1] 2165 2166 ray = [] 2167 2168 cur_target_abs_position = (float(abs_position[0]), float(abs_position[1])) 2169 cur_direction_in_degree = direction_in_degree % 360 2170 2171 # go through every reflection -> will early stop if hitting a wall (if wall-bouncing is deactivated) 2172 for cur_depth in range(reflexion_order): 2173 if should_scale: 2174 current_ray_line = [normalize_point_numba(x=cur_target_abs_position[0], y=cur_target_abs_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 2175 else: 2176 current_ray_line = [(cur_target_abs_position[0], cur_target_abs_position[1])] 2177 2178 last_abs_position = cur_target_abs_position 2179 2180 # direction 2181 rad = math.radians(cur_direction_in_degree) 2182 dx = math.cos(rad) 2183 dy = math.sin(rad) 2184 2185 eps = 1e-12 2186 if abs(dx) < eps: dx = 0.0 2187 if abs(dy) < eps: dy = 0.0 2188 2189 # start float pos and starting cell 2190 x0 = float(cur_target_abs_position[0]) 2191 y0 = float(cur_target_abs_position[1]) 2192 cell_x = int(math.floor(x0)) 2193 cell_y = int(math.floor(y0)) 2194 2195 # outside start -> handle border/reflection/exit 2196 if not (0 <= x0 < IMG_WIDTH and 0 <= y0 < IMG_HEIGHT): 2197 ray.append(current_ray_line) 2198 if img_border_also_collide: 2199 wall_vector = get_img_border_vector_numba(position=(x0, y0), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 2200 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2201 cur_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2202 cur_target_abs_position = last_abs_position 2203 continue 2204 else: 2205 return ray 2206 2207 # DDA parameters 2208 tDeltaX = math.inf if dx == 0.0 else abs(1.0 / dx) 2209 tDeltaY = math.inf if dy == 0.0 else abs(1.0 / dy) 2210 2211 if dx > 0: 2212 stepX = 1 2213 nextBoundaryX = cell_x + 1.0 2214 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 2215 elif dx < 0: 2216 stepX = -1 2217 nextBoundaryX = cell_x * 1.0 # left boundary of cell 2218 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 2219 else: 2220 stepX = 0 2221 tMaxX = math.inf 2222 2223 if dy > 0: 2224 stepY = 1 2225 nextBoundaryY = cell_y + 1.0 2226 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 2227 elif dy < 0: 2228 stepY = -1 2229 nextBoundaryY = cell_y * 1.0 2230 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 2231 else: 2232 stepY = 0 2233 tMaxY = math.inf 2234 2235 max_steps = (IMG_WIDTH + IMG_HEIGHT) * 6 2236 steps = 0 2237 last_position_saved = False 2238 2239 # immediate-start-in-wall handling 2240 if 0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT: 2241 start_pixel = float(img[cell_y, cell_x]) 2242 # if start_pixel in wall_values_set: 2243 if _is_in_wall_values(start_pixel, wall_values): 2244 # compute a collision point precisely at start (we'll use origin) 2245 # add collision point (start) and reflect 2246 hit_x = x0 2247 hit_y = y0 2248 if should_scale: 2249 current_ray_line.append(normalize_point_numba(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)) 2250 else: 2251 current_ray_line.append((hit_x, hit_y)) 2252 ray.append(current_ray_line) 2253 2254 building_angle = float(wall_map[cell_y, cell_x]) 2255 if not np.isfinite(building_angle): 2256 raise Exception("Got non-finite value from Wall-Map.") 2257 wall_vector = degree_to_vector_numba(building_angle) 2258 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2259 cur_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2260 ndx, ndy = new_direction[0], new_direction[1] 2261 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 2262 continue 2263 2264 # DDA main loop 2265 while steps < max_steps: 2266 steps += 1 2267 2268 # choose axis to step and capture t_hit (distance along ray to boundary) 2269 if tMaxX < tMaxY: 2270 t_hit = tMaxX 2271 # step in x 2272 cell_x += stepX 2273 tMaxX += tDeltaX 2274 stepped_axis = 'x' 2275 else: 2276 t_hit = tMaxY 2277 # step in y 2278 cell_y += stepY 2279 tMaxY += tDeltaY 2280 stepped_axis = 'y' 2281 2282 # compute exact collision position along ray from origin (x0,y0) 2283 hit_x = x0 + dx * t_hit 2284 hit_y = y0 + dy * t_hit 2285 2286 # For recording the traversal we can append intermediate cell centers encountered so far. 2287 # But more importantly, append the collision point to the current segment BEFORE storing it. 2288 if should_scale: 2289 current_ray_line.append(normalize_point_numba(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)) 2290 else: 2291 current_ray_line.append((hit_x, hit_y)) 2292 2293 # Now check if we've left the image bounds (cell_x, cell_y refer to the new cell we stepped into) 2294 if not (0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT): 2295 ray.append(current_ray_line) 2296 last_position_saved = True 2297 2298 if img_border_also_collide: 2299 wall_vector = get_img_border_vector_numba(position=(cell_x, cell_y), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 2300 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2301 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2302 # start next ray from last in-image position (hit_x, hit_y) nudged slightly 2303 ndx, ndy = new_direction[0], new_direction[1] 2304 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 2305 cur_direction_in_degree = new_direction_in_degree 2306 break 2307 else: 2308 return ray 2309 2310 # sample the pixel in the cell we stepped into 2311 next_pixel = float(img[cell_y, cell_x]) 2312 if _is_in_wall_values(next_pixel, wall_values): 2313 # we hit a wall — collision point already appended 2314 last_abs_position = (hit_x, hit_y) 2315 ray.append(current_ray_line) 2316 last_position_saved = True 2317 2318 building_angle = float(wall_map[cell_y, cell_x]) 2319 if not np.isfinite(building_angle): 2320 raise Exception("Got non-finite value from Wall-Map.") 2321 wall_vector = degree_to_vector_numba(building_angle) 2322 2323 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2324 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2325 2326 # start next beam from collision point nudged outwards 2327 ndx, ndy = new_direction[0], new_direction[1] 2328 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 2329 cur_direction_in_degree = new_direction_in_degree 2330 break 2331 else: 2332 # no hit -> continue marching; also add a representative point in the traversed cell (optional) 2333 # we already appended the exact hit point for this step; for smoother lines you may add cell center too 2334 last_abs_position = (hit_x, hit_y) 2335 # continue 2336 2337 # end DDA loop 2338 if not last_position_saved: 2339 ray.append(current_ray_line) 2340 2341 return ray 2342 2343 2344 2345@numba.njit(cache=True, fastmath=True) 2346def get_all_pixel_coordinates_in_between_numba(x1, y1, x2, y2): 2347 """ 2348 Get all pixel coordinates along a line between two points using Bresenham's algorithm. 2349 2350 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 2351 2352 Parameters: 2353 - x1 (int): <br> 2354 Starting x-coordinate. 2355 - y1 (int): <br> 2356 Starting y-coordinate. 2357 - x2 (int): <br> 2358 Ending x-coordinate. 2359 - y2 (int): <br> 2360 Ending y-coordinate. 2361 2362 Returns: 2363 - list: 2364 List of (x, y) tuples representing all pixels between the start and end points 2365 """ 2366 coordinates = List.empty_list(TYPE_POINT_INT) 2367 2368 dx = abs(x2 - x1) 2369 dy = abs(y2 - y1) 2370 x, y = x1, y1 2371 2372 sx = 1 if x1 < x2 else -1 2373 sy = 1 if y1 < y2 else -1 2374 2375 if dx > dy: 2376 err = dx / 2.0 2377 while x != x2: 2378 coordinates.append((x, y)) 2379 err -= dy 2380 if err < 0: 2381 y += sy 2382 err += dx 2383 x += sx 2384 else: 2385 err = dy / 2.0 2386 while y != y2: 2387 coordinates.append((x, y)) 2388 err -= dx 2389 if err < 0: 2390 x += sx 2391 err += dy 2392 y += sy 2393 2394 coordinates.append((x2, y2)) # include the last point 2395 return list(coordinates)
94class RayIterator: 95 """ 96 A container class to save every step of a ray tracing process. 97 """ 98 def __init__(self, other_ray_iterator=None): 99 """ 100 Initialize a RayIterator instance. 101 102 Parameters: 103 - other_ray_iterator (RayIterator, optional): <br> 104 An existing RayIterator to copy. If provided, creates a deep copy 105 of the other iterator's rays_collection. If None, creates an empty iterator. 106 107 Returns: <br> 108 None 109 """ 110 if other_ray_iterator is None: 111 self.rays_collection = [] 112 else: 113 self.rays_collection = copy.deepcopy(other_ray_iterator.rays_collection) 114 115 def __repr__(self): 116 """ 117 Return a string representation of the RayIterator. 118 119 Returns:<br> 120 str: String representation showing the number of iterations/time-steps. 121 """ 122 return f"RayIterator with {self.len_iterations()} iterations (time-steps)." 123 124 def __iter__(self): 125 """ 126 Make the RayIterator iterable over its rays collections. 127 128 Yields:<br> 129 list: Each iteration's collection of rays. 130 """ 131 for rays in self.rays_collection: 132 yield rays 133 134 def __getitem__(self, key): 135 """ 136 Get rays from the latest iteration using key/index. 137 138 Parameters: 139 - key (int or slice): Index or slice to access rays in the latest iteration. 140 141 Returns:<br> 142 list: Rays from the latest iteration corresponding to the key. 143 """ 144 return self.rays_collection[-1][key] 145 146 def __len__(self): 147 """ 148 Get the number of rays in the latest iteration. 149 150 Returns:<br> 151 int: Number of rays in the latest iteration. 152 """ 153 return len(self.rays_collection[-1]) 154 155 def __add__(self, other): 156 """ 157 Add another RayIterator or value to this RayIterator element-wise. 158 159 Parameters: 160 - other (RayIterator or any): <br> 161 If RayIterator: combines ray collections from both iterators. 162 If other type: adds the value to each ray in all iterations. 163 164 Returns:<br> 165 RayIterator: New RayIterator containing the combined/adjusted results. 166 167 Raises:<br> 168 TypeError: If other is not a RayIterator and addition operation fails. 169 """ 170 if isinstance(other, RayIterator): 171 new_iterator = RayIterator() 172 if self.len_iterations() > other.len_iterations(): 173 iter_1 = self 174 iter_2 = other 175 else: 176 iter_1 = other 177 iter_2 = self 178 179 iter_1 = copy.deepcopy(iter_1) 180 iter_2 = copy.deepcopy(iter_2) 181 182 for idx in range(iter_1.len_iterations()): 183 cur_addition = iter_1.get_iteration(idx) 184 if iter_2.len_iterations() > idx: 185 cur_addition += iter_2.get_iteration(idx) 186 elif iter_2.len_iterations() == 0: 187 pass 188 else: 189 cur_addition += iter_2.get_iteration(-1) 190 191 new_iterator.add_iteration(cur_addition) 192 193 return new_iterator 194 else: 195 new_iterator = RayIterator() 196 197 for idx in range(self.len_iterations()): 198 cur_addition = self.get_iteration(idx) + other 199 new_iterator.add_iteration(cur_addition) 200 201 return new_iterator 202 203 def __iadd__(self, other): 204 """ 205 In-place addition of another RayIterator or value. 206 207 Parameters: 208 - other (RayIterator or any): 209 Object to add to this RayIterator. 210 211 Returns:<br> 212 RayIterator: self, after performing the in-place addition. 213 """ 214 new_iterator = self.__add__(other) 215 self.rays_collection = new_iterator.rays_collection 216 return self 217 218 def len_iterations(self): 219 """ 220 Get the total number of iterations/time-steps stored. 221 222 Returns:<br> 223 int: <br> 224 Number of iterations in the rays_collection. 225 """ 226 return len(self.rays_collection) 227 228 def add_iteration(self, rays): 229 """ 230 Add a new iteration (collection of rays) to the iterator. 231 232 Parameters: 233 - rays (list): Collection of rays to add as a new iteration. 234 Format: rays[ray][beam][point] = (x, y) 235 236 Returns:<br> 237 RayIterator: self, for method chaining. 238 """ 239 self.rays_collection += [copy.deepcopy(rays)] 240 return self 241 242 def add_rays(self, rays): 243 """ 244 Add rays to the every iteration (in-place modification). 245 If one iterator have less steps, the last step will be used for all other iterations. 246 Which equals no change for those iterations. 247 248 Parameters: 249 - rays (list): Rays to add to the latest iteration. 250 Format: rays[ray][beam][point] = (x, y) 251 252 Returns:<br> 253 list: <br> 254 The updated rays_collection. 255 """ 256 self.rays_collection = self.__add__(copy.deepcopy(rays)).rays_collection 257 return self.rays_collection 258 259 def print_info(self): 260 """ 261 Print statistical information about the ray collections. 262 263 Displays: 264 - Number of iterations 265 - Information about the latest iteration's rays including: 266 * Number of rays, beams, reflexions, and points 267 * Mean, median, max, min, and variance for beams per ray, 268 reflexions per ray, and points per beam 269 * Value range for x and y coordinates 270 271 Returns:<br> 272 None 273 """ 274 print(f"Ray Iterator with {self.len_iterations()} iterations (time-steps).") 275 print("Latest Rays Info:\n") 276 print_rays_info(self.rays_collection[-1]) 277 # for idx, rays in enumerate(self.rays_collection): 278 # print(f"--- Rays Set {idx} ---") 279 # print_rays_info(rays) 280 281 def reduce_to_x_steps(self, x_steps): 282 """ 283 Reduce the number of stored iterations to approximately x_steps. 284 285 Uses linear sampling to keep representative iterations while reducing 286 memory usage. If x_steps is greater than current iterations, does nothing. 287 288 Parameters: 289 - x_steps (int): <br> 290 Desired number of iterations to keep. 291 292 Returns:<br> 293 None 294 """ 295 if x_steps >= self.len_iterations(): 296 return # nothing to do 297 298 step_size = self.len_iterations() / x_steps 299 new_rays_collection = [] 300 for i in range(x_steps): 301 index = int(i * step_size) 302 new_rays_collection += [self.get_iteration(index)] 303 304 self.rays_collection = new_rays_collection 305 306 def apply_and_update(self, func): 307 """ 308 Apply a function to each iteration's rays and update in-place. 309 310 Parameters: 311 - func (callable): <br> 312 Function that takes a rays collection and returns a modified rays collection. 313 Will be applied to each iteration. 314 315 Returns:<br> 316 None 317 """ 318 for i in range(self.len_iterations()): 319 self.rays_collection[i] = func(self.rays_collection[i]) 320 321 def apply_and_return(self, func): 322 """ 323 Apply a function to each iteration's rays and return results. 324 325 Parameters: 326 - func (callable): <br> 327 Function that takes a rays collection and returns some result. 328 Will be applied to each iteration. 329 330 Returns: 331 list: <br> 332 Results of applying func to each iteration's rays. 333 """ 334 results = [] 335 for i in range(self.len_iterations()): 336 results += [func(self.rays_collection[i])] 337 338 return results 339 340 def get_iteration(self, index): 341 """ 342 Get a specific iteration's rays collection. 343 344 Parameters: 345 - index (int): <br> 346 Index of the iteration to retrieve. Supports negative indexing (e.g., -1 for last iteration). 347 348 Returns: 349 list: <br> 350 - Rays collection at the specified iteration. 351 352 Raises: <br> 353 IndexError: If index is out of range. 354 """ 355 if index < -1 * self.len_iterations() or index >= self.len_iterations(): 356 raise IndexError("RayIterator index out of range.") 357 return self.rays_collection[index]
A container class to save every step of a ray tracing process.
98 def __init__(self, other_ray_iterator=None): 99 """ 100 Initialize a RayIterator instance. 101 102 Parameters: 103 - other_ray_iterator (RayIterator, optional): <br> 104 An existing RayIterator to copy. If provided, creates a deep copy 105 of the other iterator's rays_collection. If None, creates an empty iterator. 106 107 Returns: <br> 108 None 109 """ 110 if other_ray_iterator is None: 111 self.rays_collection = [] 112 else: 113 self.rays_collection = copy.deepcopy(other_ray_iterator.rays_collection)
Initialize a RayIterator instance.
Parameters:
- other_ray_iterator (RayIterator, optional):
An existing RayIterator to copy. If provided, creates a deep copy of the other iterator's rays_collection. If None, creates an empty iterator.
Returns:
None
218 def len_iterations(self): 219 """ 220 Get the total number of iterations/time-steps stored. 221 222 Returns:<br> 223 int: <br> 224 Number of iterations in the rays_collection. 225 """ 226 return len(self.rays_collection)
Get the total number of iterations/time-steps stored.
Returns:
int:
Number of iterations in the rays_collection.
228 def add_iteration(self, rays): 229 """ 230 Add a new iteration (collection of rays) to the iterator. 231 232 Parameters: 233 - rays (list): Collection of rays to add as a new iteration. 234 Format: rays[ray][beam][point] = (x, y) 235 236 Returns:<br> 237 RayIterator: self, for method chaining. 238 """ 239 self.rays_collection += [copy.deepcopy(rays)] 240 return self
Add a new iteration (collection of rays) to the iterator.
Parameters:
- rays (list): Collection of rays to add as a new iteration. Format: rays[ray][beam][point] = (x, y)
Returns:
RayIterator: self, for method chaining.
242 def add_rays(self, rays): 243 """ 244 Add rays to the every iteration (in-place modification). 245 If one iterator have less steps, the last step will be used for all other iterations. 246 Which equals no change for those iterations. 247 248 Parameters: 249 - rays (list): Rays to add to the latest iteration. 250 Format: rays[ray][beam][point] = (x, y) 251 252 Returns:<br> 253 list: <br> 254 The updated rays_collection. 255 """ 256 self.rays_collection = self.__add__(copy.deepcopy(rays)).rays_collection 257 return self.rays_collection
Add rays to the every iteration (in-place modification). If one iterator have less steps, the last step will be used for all other iterations. Which equals no change for those iterations.
Parameters:
- rays (list): Rays to add to the latest iteration. Format: rays[ray][beam][point] = (x, y)
Returns:
list:
The updated rays_collection.
259 def print_info(self): 260 """ 261 Print statistical information about the ray collections. 262 263 Displays: 264 - Number of iterations 265 - Information about the latest iteration's rays including: 266 * Number of rays, beams, reflexions, and points 267 * Mean, median, max, min, and variance for beams per ray, 268 reflexions per ray, and points per beam 269 * Value range for x and y coordinates 270 271 Returns:<br> 272 None 273 """ 274 print(f"Ray Iterator with {self.len_iterations()} iterations (time-steps).") 275 print("Latest Rays Info:\n") 276 print_rays_info(self.rays_collection[-1]) 277 # for idx, rays in enumerate(self.rays_collection): 278 # print(f"--- Rays Set {idx} ---") 279 # print_rays_info(rays)
Print statistical information about the ray collections.
Displays:
- Number of iterations
- Information about the latest iteration's rays including:
- Number of rays, beams, reflexions, and points
- Mean, median, max, min, and variance for beams per ray, reflexions per ray, and points per beam
- Value range for x and y coordinates
Returns:
None
281 def reduce_to_x_steps(self, x_steps): 282 """ 283 Reduce the number of stored iterations to approximately x_steps. 284 285 Uses linear sampling to keep representative iterations while reducing 286 memory usage. If x_steps is greater than current iterations, does nothing. 287 288 Parameters: 289 - x_steps (int): <br> 290 Desired number of iterations to keep. 291 292 Returns:<br> 293 None 294 """ 295 if x_steps >= self.len_iterations(): 296 return # nothing to do 297 298 step_size = self.len_iterations() / x_steps 299 new_rays_collection = [] 300 for i in range(x_steps): 301 index = int(i * step_size) 302 new_rays_collection += [self.get_iteration(index)] 303 304 self.rays_collection = new_rays_collection
Reduce the number of stored iterations to approximately x_steps.
Uses linear sampling to keep representative iterations while reducing memory usage. If x_steps is greater than current iterations, does nothing.
Parameters:
- x_steps (int):
Desired number of iterations to keep.
Returns:
None
306 def apply_and_update(self, func): 307 """ 308 Apply a function to each iteration's rays and update in-place. 309 310 Parameters: 311 - func (callable): <br> 312 Function that takes a rays collection and returns a modified rays collection. 313 Will be applied to each iteration. 314 315 Returns:<br> 316 None 317 """ 318 for i in range(self.len_iterations()): 319 self.rays_collection[i] = func(self.rays_collection[i])
Apply a function to each iteration's rays and update in-place.
Parameters:
- func (callable):
Function that takes a rays collection and returns a modified rays collection. Will be applied to each iteration.
Returns:
None
321 def apply_and_return(self, func): 322 """ 323 Apply a function to each iteration's rays and return results. 324 325 Parameters: 326 - func (callable): <br> 327 Function that takes a rays collection and returns some result. 328 Will be applied to each iteration. 329 330 Returns: 331 list: <br> 332 Results of applying func to each iteration's rays. 333 """ 334 results = [] 335 for i in range(self.len_iterations()): 336 results += [func(self.rays_collection[i])] 337 338 return results
Apply a function to each iteration's rays and return results.
Parameters:
- func (callable):
Function that takes a rays collection and returns some result. Will be applied to each iteration.
Returns:
list:
Results of applying func to each iteration's rays.
340 def get_iteration(self, index): 341 """ 342 Get a specific iteration's rays collection. 343 344 Parameters: 345 - index (int): <br> 346 Index of the iteration to retrieve. Supports negative indexing (e.g., -1 for last iteration). 347 348 Returns: 349 list: <br> 350 - Rays collection at the specified iteration. 351 352 Raises: <br> 353 IndexError: If index is out of range. 354 """ 355 if index < -1 * self.len_iterations() or index >= self.len_iterations(): 356 raise IndexError("RayIterator index out of range.") 357 return self.rays_collection[index]
Get a specific iteration's rays collection.
Parameters:
- index (int):
Index of the iteration to retrieve. Supports negative indexing (e.g., -1 for last iteration).
Returns:
list:
- Rays collection at the specified iteration.
Raises:
IndexError: If index is out of range.
384def print_rays_info(rays): 385 """ 386 Print statistical information about a collection of rays. 387 388 Each ray consists of multiple beams, and each beam consists of multiple points. 389 The function computes and displays statistics such as: 390 - Number of rays, beams, reflexions, and points 391 - Mean, median, max, min, and variance for beams per ray, reflexions per ray, and points per beam 392 - Value range for x and y coordinates 393 394 Parameters: 395 - rays (list): <br> 396 Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y) 397 """ 398 if isinstance(rays, RayIterator): 399 rays.print_info() 400 else: 401 nrays = 0 402 nbeams = 0 403 nbeams_per_ray = [] 404 nreflexions = 0 405 nreflexions_per_ray = [] 406 npoints = 0 407 npoints_per_beam_point = [] 408 values_per_point = [] 409 min_x_value = None 410 max_x_value = None 411 min_y_value = None 412 max_y_value = None 413 for ray in rays: 414 nrays += 1 415 cur_beams = 0 416 cur_reflexions = 0 417 for beam_points in ray: 418 nbeams += 1 419 nreflexions += 1 420 cur_beams += 1 421 cur_reflexions += 1 422 cur_points = 0 423 for x in beam_points: 424 npoints += 1 425 cur_points += 1 426 values_per_point += [len(x)] 427 min_x_value = x[0] if min_x_value is None else min(min_x_value, x[0]) 428 max_x_value = x[0] if max_x_value is None else max(max_x_value, x[0]) 429 min_y_value = x[1] if min_y_value is None else min(min_y_value, x[1]) 430 max_y_value = x[1] if max_y_value is None else max(max_y_value, x[1]) 431 npoints_per_beam_point += [cur_points] 432 nreflexions -= 1 433 cur_reflexions -= 1 434 nreflexions_per_ray += [cur_reflexions] 435 nbeams_per_ray += [cur_beams] 436 437 print(f"Rays: {nrays}") 438 print(f"Beams: {nbeams}") 439 print(f" - Mean Beams per Ray: {round(np.mean(nbeams_per_ray), 1)}") 440 print(f" - Median: {round(np.median(nbeams_per_ray), 1)}") 441 print(f" - Max: {round(np.max(nbeams_per_ray), 1)}") 442 print(f" - Min: {round(np.min(nbeams_per_ray), 1)}") 443 print(f" - Variance: {round(np.std(nbeams_per_ray), 1)}") 444 print(f"Reflexions: {nreflexions}") 445 print(f" - Mean Reflexions per Ray: {round(np.mean(nreflexions_per_ray), 1)}") 446 print(f" - Median: {round(np.median(nreflexions_per_ray), 1)}") 447 print(f" - Max: {round(np.max(nreflexions_per_ray), 1)}") 448 print(f" - Min: {round(np.min(nreflexions_per_ray), 1)}") 449 print(f" - Variance: {round(np.std(nreflexions_per_ray), 1)}") 450 print(f"Points: {npoints}") 451 print(f" - Mean Points per Beam: {round(np.mean(npoints_per_beam_point), 1)}") 452 print(f" - Median: {round(np.median(npoints_per_beam_point), 1)}") 453 print(f" - Max: {round(np.max(npoints_per_beam_point), 1)}") 454 print(f" - Min: {round(np.min(npoints_per_beam_point), 1)}") 455 print(f" - Variance: {round(np.std(npoints_per_beam_point), 1)}") 456 print(f" - Mean Point Values: {round(np.mean(values_per_point), 1)}") 457 print(f" - Median: {round(np.median(values_per_point), 1)}") 458 print(f" - Variance: {round(np.std(values_per_point), 1)}") 459 print(f"\nValue-Range:\n x ∈ [{min_x_value:.2f}, {max_x_value:.2f}]\n y ∈ [{min_y_value:.2f}, {max_y_value:.2f}]") 460 # [ inclusive, ( number is not included 461 462 if nrays > 0: 463 print(f"\nExample:\nRay 1, beams: {len(rays[0])}") 464 if nbeams > 0: 465 print(f"Ray 1, beam 1, points: {len(rays[0][0])}") 466 if npoints > 0: 467 print(f"Ray 1, beam 1, point 1: {len(rays[0][0][0])}") 468 print("\n")
Print statistical information about a collection of rays.
Each ray consists of multiple beams, and each beam consists of multiple points. The function computes and displays statistics such as:
- Number of rays, beams, reflexions, and points
- Mean, median, max, min, and variance for beams per ray, reflexions per ray, and points per beam
- Value range for x and y coordinates
Parameters:
- rays (list):
Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y)
472def save(path, rays): 473 """ 474 Save a list of rays to a text file. 475 476 The rays are serialized using a simple text-based format. 477 Each ray is delimited by '>' and '<', and each point is represented as "x | y". 478 479 Parameters: 480 - path (str): <br> 481 Path to the file where data should be saved. If no '.txt' extension is present, it will be appended automatically. 482 - rays (list): <br> 483 Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y) 484 485 Returns:<br> 486 None 487 """ 488 if isinstance(rays, RayIterator): 489 if not path.endswith(".pkl"): 490 path += ".pkl" 491 pickle.dump(rays, builtins.open(path, "wb")) 492 return 493 494 # transform rays into an string 495 rays_str = "" 496 for ray in rays: 497 rays_str += ">\n" 498 for beam in ray: 499 rays_str += "\n" 500 for cur_point in beam: 501 rays_str += f"{cur_point[0]} | {cur_point[1]}, " 502 rays_str += "<\n" 503 504 rays_str = rays_str.replace("\n\n", "\n") 505 506 if not path.endswith(".txt"): 507 path += ".txt" 508 509 with builtins.open(path, "w") as file_: 510 file_.write(rays_str)
Save a list of rays to a text file.
The rays are serialized using a simple text-based format. Each ray is delimited by '>' and '<', and each point is represented as "x | y".
Parameters:
- path (str):
Path to the file where data should be saved. If no '.txt' extension is present, it will be appended automatically. - rays (list):
Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y)
Returns:
None
514def open(path, is_iterator=False) -> list: 515 """ 516 Open and parse a ray text file into a structured list. 517 518 The file is expected to follow the same format as produced by `save()`. 519 520 Parameters: 521 - path (str): <br> 522 Path to the .txt file containing ray data. 523 524 Returns: 525 - list: <br> 526 Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y) 527 """ 528 if is_iterator or path.endswith(".pkl"): 529 if not path.endswith(".pkl"): 530 path += ".pkl" 531 rays = pickle.load(builtins.open(path, "rb")) 532 return rays 533 534 if not path.endswith(".txt"): 535 path += ".txt" 536 537 with builtins.open(path, "r") as file_: 538 content = file_.read().strip() 539 540 rays = [] 541 for ray in content.split(">"): 542 extracted_ray = [] 543 for beam in ray.split("\n"): 544 extracted_beam = [] 545 beam = beam.strip() 546 if not beam or beam == "<": 547 continue 548 549 for point in beam.split(","): 550 point = point.strip() 551 if not point or point == "<": 552 continue 553 554 try: 555 point_x, point_y = point.strip().split("|") 556 except Exception as e: 557 print("Point of error:", point) 558 raise e 559 560 extracted_beam += [(float(point_x), float(point_y))] 561 if len(extracted_beam) > 0: 562 extracted_ray += [extracted_beam] 563 if len(extracted_ray) > 0: 564 rays += [extracted_ray] 565 return rays
Open and parse a ray text file into a structured list.
The file is expected to follow the same format as produced by save().
Parameters:
- path (str):
Path to the .txt file containing ray data.
Returns:
- list:
Nested list structure representing rays. Format: rays[ray][beam][point] = (x, y)
569def merge(rays_1, rays_2, *other_rays_): 570 """ 571 Merge multiple ray datasets into a single list. 572 573 Parameters: 574 - rays_1 (list): <br> 575 First set of rays. 576 - rays_2 (list): <br> 577 Second set of rays. 578 - *other_rays_ (list): <br> 579 Additional ray lists to merge. 580 581 Returns: 582 - list: 583 Combined list of all rays. 584 """ 585 merged = rays_1 + rays_2 586 587 for rays in other_rays_: 588 merged += rays 589 590 return merged
Merge multiple ray datasets into a single list.
Parameters:
- rays_1 (list):
First set of rays. - rays_2 (list):
Second set of rays. - *other_rays_ (list):
Additional ray lists to merge.
Returns:
- list: Combined list of all rays.
599def get_all_pixel_coordinates_in_between(x1, y1, x2, y2): 600 """ 601 Get all pixel coordinates along a line between two points using Bresenham's algorithm. 602 603 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 604 605 Parameters: 606 - x1 (int): <br> 607 Starting x-coordinate. 608 - y1 (int): <br> 609 Starting y-coordinate. 610 - x2 (int): <br> 611 Ending x-coordinate. 612 - y2 (int): <br> 613 Ending y-coordinate. 614 615 Returns: 616 - list: 617 List of (x, y) tuples representing all pixels between the start and end points 618 """ 619 coordinates = [] 620 621 dx = abs(x2 - x1) 622 dy = abs(y2 - y1) 623 x, y = x1, y1 624 625 sx = 1 if x1 < x2 else -1 626 sy = 1 if y1 < y2 else -1 627 628 if dx > dy: 629 err = dx / 2.0 630 while x != x2: 631 coordinates += [(x, y)] 632 err -= dy 633 if err < 0: 634 y += sy 635 err += dx 636 x += sx 637 else: 638 err = dy / 2.0 639 while y != y2: 640 coordinates += [(x, y)] 641 err -= dx 642 if err < 0: 643 x += sx 644 err += dy 645 y += sy 646 647 coordinates += [(x2, y2)] # include the last point 648 return coordinates
Get all pixel coordinates along a line between two points using Bresenham's algorithm.
https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
Parameters:
- x1 (int):
Starting x-coordinate. - y1 (int):
Starting y-coordinate. - x2 (int):
Ending x-coordinate. - y2 (int):
Ending y-coordinate.
Returns:
- list: List of (x, y) tuples representing all pixels between the start and end points
652def get_wall_map(img, wall_values=None, thickness=1, 653 use_numba_compilation=False): 654 """ 655 Generate a wall map where each pixel encodes the wall orientation (in degrees). 656 657 Parameters: 658 - img (numpy.ndarray): <br> 659 Input image representing scene or segmentation mask. 660 - wall_values (list, optional): <br> 661 Specific pixel values considered as walls. If None, all non-zero pixels are treated as walls. 662 - thickness (int, optional): <br> 663 Thickness of wall lines (default is 1). 664 - use_numba_compilation (bool, optional):<br> 665 Whether to use the compiled (to machine code) version of compute heavy functions. 666 667 Returns: 668 - numpy.ndarray: 669 2D array (same width and height as input) 670 where each wall pixel contains the wall angle in degrees (0-360), 671 and non-wall pixels are set to infinity (np.inf). 672 """ 673 # numba optimization -> change function locally 674 if use_numba_compilation: 675 get_all_pixel_coordinates_in_between_ = get_all_pixel_coordinates_in_between_numba 676 else: 677 get_all_pixel_coordinates_in_between_ = get_all_pixel_coordinates_in_between 678 679 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 680 wall_map = np.full((IMG_HEIGHT, IMG_WIDTH), np.inf, dtype=np.uint16) # uint16 to get at least 360 degree/value range 681 682 # only detect edges from objects with specific pixel values 683 if wall_values is not None: 684 mask = np.isin(img, wall_values).astype(np.uint8) * 255 685 else: 686 mask = img 687 if np.max(mask) < 64: 688 mask = mask.astype(np.uint8) * 255 689 690 # detect edges and contours 691 edges = cv2.Canny(mask, 100, 200) 692 contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 693 694 # convert contours to line segments 695 for c in contours: 696 for i in range(len(c)-1): 697 x1, y1 = c[i][0] 698 x2, y2 = c[i+1][0] 699 dy = y2 - y1 700 dx = x2 - x1 701 angle = math.atan2(dy, dx) 702 angle_deg = math.degrees(angle) 703 for x, y in get_all_pixel_coordinates_in_between_(x1, y1, x2, y2): 704 # wall_map[y, x] = int(angle_deg) % 360 705 706 for tx in range(-thickness, thickness+1): 707 for ty in range(-thickness, thickness+1): 708 nx, ny = x+tx, y+ty 709 if 0 <= nx < IMG_WIDTH and 0 <= ny < IMG_HEIGHT: 710 wall_map[ny, nx] = int(angle_deg) % 360 711 return wall_map
Generate a wall map where each pixel encodes the wall orientation (in degrees).
Parameters:
- img (numpy.ndarray):
Input image representing scene or segmentation mask. - wall_values (list, optional):
Specific pixel values considered as walls. If None, all non-zero pixels are treated as walls. - thickness (int, optional):
Thickness of wall lines (default is 1). - use_numba_compilation (bool, optional):
Whether to use the compiled (to machine code) version of compute heavy functions.
Returns:
- numpy.ndarray: 2D array (same width and height as input) where each wall pixel contains the wall angle in degrees (0-360), and non-wall pixels are set to infinity (np.inf).
715def update_pixel_position(direction_in_degree, cur_position, target_line): 716 """ 717 Update the pixel position of a moving point toward a target line based on direction and proximity. 718 719 Combines the direction vector with a vector pointing toward the closest point 720 on the target line, ensuring pixel-wise movement (discrete steps). 721 722 Parameters: 723 - direction_in_degree (float): <br> 724 Movement direction in degrees. 725 - cur_position (tuple): <br> 726 Current pixel position (x, y). 727 - target_line (list): <br> 728 Target line defined as [x1, y1, x2, y2]. 729 730 Returns: 731 - tuple: 732 Updated pixel position (x, y). 733 """ 734 # 1. Calc distance from point to target line 735 736 # perpendicular vector to line (points toward line) 737 point = np.array(cur_position) 738 line_start_point = np.array(target_line[0:2]) 739 line_end_point = np.array(target_line[2:4]) 740 741 # projection along the line -> throw the point vector vertical/perpendicular on the line and see where it cuts with normed AP to AB 742 # t is the length from point to the line, therefore it gets normed 743 t = np.dot(point - line_start_point, line_end_point - line_start_point) / (np.dot(line_end_point - line_start_point, line_end_point - line_start_point) + 1e-8) 744 745 # limit it to the line id needed -> because we don't want smaller or bigger values than that 746 # -> 0 would be point A 747 # -> 1 would be point B 748 # t = np.clip(t, 0, 1) 749 if t < 0.0: 750 t = 0.0 751 elif t > 1.0: 752 t = 1.0 753 754 # get closest point by applying the found t as lentgh from startpoint in the line vector direction 755 closest = line_start_point + t * (line_end_point - line_start_point) 756 757 # get the final vector to the line 758 to_line = closest - point # vector from current pos to closest point on line 759 760 # 2. Calc vector to the degree 761 # movement vector based on angle 762 rad = math.radians(direction_in_degree) 763 move_dir = np.array([math.cos(rad), math.sin(rad)]) 764 765 # 3. Combine vector to the line and degree vector 766 # combine movement towards direction and towards line 767 combined = move_dir + to_line * 0.5 # weighting factor 768 769 # pick pixel step (continuous to discrete) -> [-1, 0, 1] 770 step_x = np.sign(combined[0]) 771 step_y = np.sign(combined[1]) 772 773 # clamp to [-1, 1], if bigger/smaller 774 # step_x = int(np.clip(step_x, -1, 1)) 775 if step_x < 0.0: 776 step_x = 0.0 777 elif step_x > 1.0: 778 step_x = 1.0 779 # step_y = int(np.clip(step_y, -1, 1)) 780 if step_y < 0.0: 781 step_y = 0.0 782 elif step_y > 1.0: 783 step_y = 1.0 784 785 return (int(cur_position[0] + step_x), int(cur_position[1] + step_y))
Update the pixel position of a moving point toward a target line based on direction and proximity.
Combines the direction vector with a vector pointing toward the closest point on the target line, ensuring pixel-wise movement (discrete steps).
Parameters:
- direction_in_degree (float):
Movement direction in degrees. - cur_position (tuple):
Current pixel position (x, y). - target_line (list):
Target line defined as [x1, y1, x2, y2].
Returns:
- tuple: Updated pixel position (x, y).
789def calc_reflection(collide_vector, wall_vector): 790 """ 791 Calculate the reflection of a collision vector against a wall vector. 792 793 The reflection is computed using the wall's normal vector and the formula: 794 r = v - 2 * (v · n) * n 795 796 Parameters: 797 - collide_vector (array-like): <br> 798 Incoming vector (2D). 799 - wall_vector (array-like): <br> 800 Wall direction vector (2D). 801 802 Returns: 803 - numpy.ndarray: 804 Reflected 2D vector. 805 """ 806 # normalize both 807 collide_vector = np.array(collide_vector, dtype=np.float64) 808 collide_vector /= np.linalg.norm(collide_vector) 809 wall_vector = np.array(wall_vector, dtype=np.float64) 810 wall_vector /= np.linalg.norm(wall_vector) 811 812 # calculate the normal of the wall 813 normal_wall_vector_1 = np.array([-wall_vector[1], wall_vector[0]]) # rotated +90° 814 normal_wall_vector_2 = np.array([wall_vector[1], -wall_vector[0]]) # rotated -90° 815 816 # decide which vector is the right one 817 # -> dot product tells which normal faces the incoming vector 818 # -> dor product shows how similiar 2 vectors are => smaller 0 means they show against each other => right vector 819 if np.dot(collide_vector, normal_wall_vector_1) < 0: 820 normal_wall_vector = normal_wall_vector_1 821 else: 822 normal_wall_vector = normal_wall_vector_2 823 824 # calc the reflection 825 return collide_vector - 2 * np.dot(collide_vector, normal_wall_vector) * normal_wall_vector
Calculate the reflection of a collision vector against a wall vector.
The reflection is computed using the wall's normal vector and the formula: r = v - 2 * (v · n) * n
Parameters:
- collide_vector (array-like):
Incoming vector (2D). - wall_vector (array-like):
Wall direction vector (2D).
Returns:
- numpy.ndarray: Reflected 2D vector.
828def get_img_border_vector(position, max_width, max_height): 829 """ 830 Determine the wall normal vector for an image border collision. 831 832 Parameters: 833 - position (tuple): <br> 834 Current position (x, y). 835 - max_width (int): <br> 836 Image width. 837 - max_height (int): <br> 838 Image height. 839 840 Returns: 841 - tuple: 842 Border wall vector corresponding to the collision side. 843 """ 844 # print(f"got {position=}") 845 if position[0] < 0: 846 return (0, 1) 847 elif position[0] >= max_width: 848 return (0, 1) 849 elif position[1] < 0: 850 return (1, 0) 851 elif position[1] >= max_height: 852 return (1, 0) 853 else: 854 # should never reach that! 855 return (0, 0)
Determine the wall normal vector for an image border collision.
Parameters:
- position (tuple):
Current position (x, y). - max_width (int):
Image width. - max_height (int):
Image height.
Returns:
- tuple: Border wall vector corresponding to the collision side.
858def trace_beam(abs_position, 859 img, 860 direction_in_degree, 861 wall_map, 862 wall_values, 863 img_border_also_collide=False, 864 reflexion_order=3, 865 should_scale=True, 866 should_return_iterative=False, 867 remove_iterative=True): 868 """ 869 Trace a ray (beam) through an image with walls and reflections. 870 871 The beam starts from a given position and follows a direction until it hits 872 a wall or border. On collisions, reflections are computed using wall normals. 873 874 Parameters: 875 - abs_position (tuple): <br> 876 Starting position (x, y) of the beam. 877 - img (numpy.ndarray): <br> 878 Input image or segmentation map. 879 - direction_in_degree (float): <br> 880 Initial direction angle of the beam. 881 - wall_map (numpy.ndarray): <br> 882 Map containing wall orientations in degrees. 883 - wall_values (list): <br> 884 List of pixel values representing walls. 885 - img_border_also_collide (bool, optional): <br> 886 Whether the image border acts as a collider (default: False). 887 - reflexion_order (int, optional): <br> 888 Number of allowed reflections (default: 3). 889 - should_scale (bool, optional): <br> 890 Whether to normalize positions to [0, 1] (default: True). 891 - should_return_iterative (bool, optional): <br> 892 Whether to return a RayIterator for step-by-step analysis (default: False). 893 - remove_iterative (bool, optional): <br> 894 Whether to optimize (ignore) ray-iterator -> only in use if should_return_iterative is False. 895 896 Returns: 897 - list: 898 Nested list structure representing the traced ray and its reflections. 899 Format: ray[beam][point] = (x, y) 900 """ 901 reflexion_order += 1 # Reflexion Order == 0 means, no reflections, therefore only 1 loop 902 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 903 904 ray = [] 905 if should_return_iterative or not remove_iterative: 906 ray_iterator = RayIterator() 907 908 cur_target_abs_position = abs_position 909 cur_direction_in_degree = direction_in_degree % 360 910 911 for cur_depth in range(reflexion_order): 912 # print(f"(Reflexion Order '{cur_depth}') {ray=}") 913 if should_scale: 914 current_ray_line = [normalize_point(x=cur_target_abs_position[0], y=cur_target_abs_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 915 else: 916 current_ray_line = [(cur_target_abs_position[0], cur_target_abs_position[1])] 917 if should_return_iterative or not remove_iterative: 918 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 919 920 last_abs_position = cur_target_abs_position 921 922 # calculate a target line to update the pixels 923 # target vector 924 dx = math.cos(math.radians(cur_direction_in_degree)) 925 dy = math.sin(math.radians(cur_direction_in_degree)) 926 target_line = [cur_target_abs_position[0], cur_target_abs_position[1], cur_target_abs_position[0], cur_target_abs_position[1]] 927 while (0 <= target_line[2] <= IMG_WIDTH) and (0 <= target_line[3] <= IMG_HEIGHT): 928 target_line[2] += 0.01 * dx 929 target_line[3] += 0.01 * dy 930 931 # update current ray 932 current_position = cur_target_abs_position 933 while True: 934 # update position 935 current_position = update_pixel_position(direction_in_degree=cur_direction_in_degree, cur_position=current_position, target_line=target_line) 936 # for current_position in get_all_pixel_coordinates_in_between(current_position[0], current_position[1], target_line[2], target_line[3]): 937 # last_position_saved = False 938 939 # check if ray is at end 940 if not (0 <= current_position[0] < IMG_WIDTH and 0 <= current_position[1] < IMG_HEIGHT): 941 ray += [current_ray_line] 942 943 if img_border_also_collide: 944 # get reflection angle 945 wall_vector = get_img_border_vector(position=current_position, 946 max_width=IMG_WIDTH, 947 max_height=IMG_HEIGHT) 948 949 # calc new direct vector 950 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 951 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 952 953 # start new beam calculation 954 cur_target_abs_position = last_abs_position 955 cur_direction_in_degree = new_direction_in_degree 956 break 957 else: 958 if should_return_iterative: 959 return ray_iterator 960 return ray 961 962 next_pixel = img[int(current_position[1]), int(current_position[0])] 963 964 # check if hit building 965 if float(next_pixel) in wall_values: 966 last_abs_position = (current_position[0], current_position[1]) 967 ray += [current_ray_line] 968 969 # get building wall reflection angle 970 building_angle = wall_map[int(current_position[1]), int(current_position[0])] 971 if building_angle == np.inf: 972 raise Exception("Got inf value from Wall-Map.") 973 wall_vector = degree_to_vector(building_angle) 974 975 # calc new direct vector 976 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 977 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 978 979 # start new beam calculation 980 cur_target_abs_position = last_abs_position 981 cur_direction_in_degree = new_direction_in_degree 982 break 983 else: 984 # update current ray 985 if should_scale: 986 current_ray_line += [normalize_point(x=current_position[0], y=current_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 987 else: 988 current_ray_line += [(current_position[0], current_position[1])] 989 if should_return_iterative or not remove_iterative: 990 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 991 last_abs_position = (current_position[0], current_position[1]) 992 993 if should_return_iterative: 994 return ray_iterator 995 return ray
Trace a ray (beam) through an image with walls and reflections.
The beam starts from a given position and follows a direction until it hits a wall or border. On collisions, reflections are computed using wall normals.
Parameters:
- abs_position (tuple):
Starting position (x, y) of the beam. - img (numpy.ndarray):
Input image or segmentation map. - direction_in_degree (float):
Initial direction angle of the beam. - wall_map (numpy.ndarray):
Map containing wall orientations in degrees. - wall_values (list):
List of pixel values representing walls. - img_border_also_collide (bool, optional):
Whether the image border acts as a collider (default: False). - reflexion_order (int, optional):
Number of allowed reflections (default: 3). - should_scale (bool, optional):
Whether to normalize positions to [0, 1] (default: True). - should_return_iterative (bool, optional):
Whether to return a RayIterator for step-by-step analysis (default: False). - remove_iterative (bool, optional):
Whether to optimize (ignore) ray-iterator -> only in use if should_return_iterative is False.
Returns:
- list: Nested list structure representing the traced ray and its reflections. Format: ray[beam][point] = (x, y)
999def trace_beam_with_DDA(abs_position, 1000 img, 1001 direction_in_degree, 1002 wall_map, 1003 wall_values, 1004 img_border_also_collide=False, 1005 reflexion_order=3, 1006 should_scale=True, 1007 should_return_iterative=False, 1008 remove_iterative=True): 1009 """ 1010 Trace a ray (beam) through a 2D image using a DDA (Digital Differential Analyzer) 1011 algorithm with precise collision points and physically accurate reflections. 1012 1013 The beam starts at a given floating-point position and marches through the grid 1014 until it intersects a wall or exits the image. For each collision, the exact 1015 hit position is computed using the ray Parameters t_hit, ensuring that reflected 1016 segments contain meaningful geometry rather than single-point artifacts. 1017 Reflections are computed using wall normals derived from the `wall_map`. 1018 1019 Parameters: 1020 - abs_position (tuple of float): <br> 1021 Starting position (x, y) of the beam in absolute pixel space. 1022 - img (numpy.ndarray): <br> 1023 2D array representing the scene. Pixel values listed in `wall_values` 1024 are treated as solid walls. 1025 - direction_in_degree (float): <br> 1026 Initial direction of the beam in degrees (0° = right, 90° = down). 1027 - wall_map (numpy.ndarray): <br> 1028 A map storing wall orientations in degrees for each pixel marked as a wall. 1029 These angles define the wall normals used for reflection. 1030 - wall_values (list, tuple, set, float, optional): <br> 1031 Pixel values identifying walls. Any pixel in this list causes a collision. 1032 If None, pixel value 0.0 is treated as a wall. 1033 - img_border_also_collide (bool, optional): <br> 1034 If True, the image borders behave like reflective walls. If False, 1035 the ray terminates when leaving the image. Default: False. 1036 - reflexion_order (int, optional): <br> 1037 Maximum number of reflections. The ray can rebound this many times before 1038 the function terminates. Default: 3. 1039 - should_scale (bool, optional): <br> 1040 If True, all emitted points (x, y) are normalized to [0, 1] range. 1041 Otherwise absolute pixel positions are returned. Default: True. 1042 - should_return_iterative (bool, optional): <br> 1043 Whether to return a RayIterator for step-by-step analysis (default: False). 1044 - remove_iterative (bool, optional): <br> 1045 Whether to optimize (ignore) ray-iterator -> only in use if should_return_iterative is False. 1046 1047 Returns: 1048 - list: 1049 Nested list structure representing the traced ray and its reflections. 1050 Format: ray[beam][point] = (x, y) 1051 """ 1052 reflexion_order += 1 1053 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 1054 1055 ray = [] 1056 if should_return_iterative or not remove_iterative: 1057 ray_iterator = RayIterator() 1058 1059 cur_target_abs_position = (float(abs_position[0]), float(abs_position[1])) 1060 cur_direction_in_degree = direction_in_degree % 360 1061 1062 # Normalize wall_values to set of floats 1063 if wall_values is None: 1064 wall_values_set = {0.0} 1065 elif isinstance(wall_values, (list, tuple, set)): 1066 for idx, v in enumerate(wall_values): 1067 if idx == 0: 1068 wall_values_set = {float(v)} 1069 else: 1070 wall_values_set.add(float(v)) 1071 else: 1072 wall_values_set = {float(wall_values)} 1073 1074 # go through every reflection -> will early stop if hitting a wall (if wall-bouncing is deactivated) 1075 for cur_depth in range(reflexion_order): 1076 if should_scale: 1077 current_ray_line = [normalize_point(x=cur_target_abs_position[0], y=cur_target_abs_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 1078 else: 1079 current_ray_line = [(cur_target_abs_position[0], cur_target_abs_position[1])] 1080 if should_return_iterative or not remove_iterative: 1081 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 1082 1083 last_abs_position = cur_target_abs_position 1084 1085 # direction 1086 rad = math.radians(cur_direction_in_degree) 1087 dx = math.cos(rad) 1088 dy = math.sin(rad) 1089 1090 eps = 1e-12 1091 if abs(dx) < eps: dx = 0.0 1092 if abs(dy) < eps: dy = 0.0 1093 1094 # start float pos and starting cell 1095 x0 = float(cur_target_abs_position[0]) 1096 y0 = float(cur_target_abs_position[1]) 1097 cell_x = int(math.floor(x0)) 1098 cell_y = int(math.floor(y0)) 1099 1100 # outside start -> handle border/reflection/exit 1101 if not (0 <= x0 < IMG_WIDTH and 0 <= y0 < IMG_HEIGHT): 1102 ray += [current_ray_line] 1103 1104 if img_border_also_collide: 1105 wall_vector = get_img_border_vector(position=(x0, y0), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 1106 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1107 cur_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1108 cur_target_abs_position = last_abs_position 1109 continue 1110 else: 1111 if should_return_iterative: 1112 return ray_iterator 1113 return ray 1114 1115 # DDA parameters 1116 tDeltaX = math.inf if dx == 0.0 else abs(1.0 / dx) 1117 tDeltaY = math.inf if dy == 0.0 else abs(1.0 / dy) 1118 1119 if dx > 0: 1120 stepX = 1 1121 nextBoundaryX = cell_x + 1.0 1122 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 1123 elif dx < 0: 1124 stepX = -1 1125 nextBoundaryX = cell_x * 1.0 # left boundary of cell 1126 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 1127 else: 1128 stepX = 0 1129 tMaxX = math.inf 1130 1131 if dy > 0: 1132 stepY = 1 1133 nextBoundaryY = cell_y + 1.0 1134 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 1135 elif dy < 0: 1136 stepY = -1 1137 nextBoundaryY = cell_y * 1.0 1138 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 1139 else: 1140 stepY = 0 1141 tMaxY = math.inf 1142 1143 max_steps = (IMG_WIDTH + IMG_HEIGHT) * 6 1144 steps = 0 1145 last_position_saved = False 1146 1147 # immediate-start-in-wall handling 1148 if 0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT: 1149 start_pixel = float(img[cell_y, cell_x]) 1150 if start_pixel in wall_values_set: 1151 # compute a collision point precisely at start (we'll use origin) 1152 # add collision point (start) and reflect 1153 hit_x = x0 1154 hit_y = y0 1155 if should_scale: 1156 current_ray_line += [normalize_point(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)] 1157 else: 1158 current_ray_line += [(hit_x, hit_y)] 1159 if should_return_iterative or not remove_iterative: 1160 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 1161 ray += [current_ray_line] 1162 1163 building_angle = float(wall_map[cell_y, cell_x]) 1164 if not np.isfinite(building_angle): 1165 raise Exception("Got non-finite value from Wall-Map.") 1166 wall_vector = degree_to_vector(building_angle) 1167 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1168 cur_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1169 ndx, ndy = new_direction[0], new_direction[1] 1170 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 1171 continue 1172 1173 # DDA main loop 1174 while steps < max_steps: 1175 steps += 1 1176 1177 # choose axis to step and capture t_hit (distance along ray to boundary) 1178 if tMaxX < tMaxY: 1179 t_hit = tMaxX 1180 # step in x 1181 cell_x += stepX 1182 tMaxX += tDeltaX 1183 else: 1184 t_hit = tMaxY 1185 # step in y 1186 cell_y += stepY 1187 tMaxY += tDeltaY 1188 1189 # compute exact collision position along ray from origin (x0,y0) 1190 hit_x = x0 + dx * t_hit 1191 hit_y = y0 + dy * t_hit 1192 1193 # For recording the traversal we can append intermediate cell centers encountered so far. 1194 # But more importantly, append the collision point to the current segment BEFORE storing it. 1195 if should_scale: 1196 current_ray_line += [normalize_point(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)] 1197 else: 1198 current_ray_line += [(hit_x, hit_y)] 1199 if should_return_iterative or not remove_iterative: 1200 ray_iterator.add_iteration([copy.deepcopy(ray)+[current_ray_line]]) 1201 1202 # Now check if we've left the image bounds (cell_x, cell_y refer to the new cell we stepped into) 1203 if not (0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT): 1204 ray += [current_ray_line] 1205 last_position_saved = True 1206 1207 if img_border_also_collide: 1208 wall_vector = get_img_border_vector(position=(cell_x, cell_y), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 1209 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1210 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1211 # start next ray from last in-image position (hit_x, hit_y) nudged slightly 1212 ndx, ndy = new_direction[0], new_direction[1] 1213 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 1214 cur_direction_in_degree = new_direction_in_degree 1215 break 1216 else: 1217 if should_return_iterative: 1218 return ray_iterator 1219 return ray 1220 1221 # sample the pixel in the cell we stepped into 1222 next_pixel = float(img[cell_y, cell_x]) 1223 if next_pixel in wall_values_set: 1224 # we hit a wall — collision point already appended 1225 last_abs_position = (hit_x, hit_y) 1226 ray += [current_ray_line] 1227 last_position_saved = True 1228 1229 building_angle = float(wall_map[cell_y, cell_x]) 1230 if not np.isfinite(building_angle): 1231 raise Exception("Got non-finite value from Wall-Map.") 1232 wall_vector = degree_to_vector(building_angle) 1233 1234 new_direction = calc_reflection(collide_vector=degree_to_vector(cur_direction_in_degree), wall_vector=wall_vector) 1235 new_direction_in_degree = vector_to_degree(new_direction[0], new_direction[1]) 1236 1237 # start next beam from collision point nudged outwards 1238 ndx, ndy = new_direction[0], new_direction[1] 1239 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 1240 cur_direction_in_degree = new_direction_in_degree 1241 break 1242 else: 1243 # no hit -> continue marching; also add a representative point in the traversed cell (optional) 1244 # we already appended the exact hit point for this step; for smoother lines you may add cell center too 1245 last_abs_position = (hit_x, hit_y) 1246 # continue 1247 1248 # end DDA loop 1249 if not last_position_saved: 1250 ray.append(current_ray_line) 1251 1252 if should_return_iterative: 1253 return ray_iterator 1254 return ray
Trace a ray (beam) through a 2D image using a DDA (Digital Differential Analyzer) algorithm with precise collision points and physically accurate reflections.
The beam starts at a given floating-point position and marches through the grid
until it intersects a wall or exits the image. For each collision, the exact
hit position is computed using the ray Parameters t_hit, ensuring that reflected
segments contain meaningful geometry rather than single-point artifacts.
Reflections are computed using wall normals derived from the wall_map.
Parameters:
- abs_position (tuple of float):
Starting position (x, y) of the beam in absolute pixel space. - img (numpy.ndarray):
2D array representing the scene. Pixel values listed inwall_valuesare treated as solid walls. - direction_in_degree (float):
Initial direction of the beam in degrees (0° = right, 90° = down). - wall_map (numpy.ndarray):
A map storing wall orientations in degrees for each pixel marked as a wall. These angles define the wall normals used for reflection. - wall_values (list, tuple, set, float, optional):
Pixel values identifying walls. Any pixel in this list causes a collision. If None, pixel value 0.0 is treated as a wall. - img_border_also_collide (bool, optional):
If True, the image borders behave like reflective walls. If False, the ray terminates when leaving the image. Default: False. - reflexion_order (int, optional):
Maximum number of reflections. The ray can rebound this many times before the function terminates. Default: 3. - should_scale (bool, optional):
If True, all emitted points (x, y) are normalized to [0, 1] range. Otherwise absolute pixel positions are returned. Default: True. - should_return_iterative (bool, optional):
Whether to return a RayIterator for step-by-step analysis (default: False). - remove_iterative (bool, optional):
Whether to optimize (ignore) ray-iterator -> only in use if should_return_iterative is False.
Returns:
- list: Nested list structure representing the traced ray and its reflections. Format: ray[beam][point] = (x, y)
1258def trace_beams(rel_position, 1259 img_src, 1260 directions_in_degree, 1261 wall_values, 1262 wall_thickness=0, 1263 img_border_also_collide=False, 1264 reflexion_order=3, 1265 should_scale_rays=True, 1266 should_scale_img=True, 1267 use_dda=True, 1268 iterative_tracking=False, 1269 iterative_steps=None, 1270 parallelization=-1, 1271 parallelization_method="processes", # "threads", "processes" 1272 use_numba_compilation=True, 1273 ignore_iterative_optimization=True): 1274 """ 1275 Trace multiple rays (beams) from a single position through an image with walls and reflections. 1276 1277 Each beam starts from a given relative position and follows its assigned direction 1278 until it collides with a wall or image border. On collisions, reflections are 1279 computed based on local wall normals extracted from the image. 1280 1281 Parameters: 1282 - rel_position (tuple): <br> 1283 Relative starting position (x, y) in normalized coordinates [0-1]. 1284 - img_src (str or numpy.ndarray): <br> 1285 Input image (array or file path) used for wall detection. 1286 - directions_in_degree (list): <br> 1287 List of beam direction angles (in degrees). 1288 - wall_values (list or float or None): <br> 1289 Pixel values representing walls or obstacles. 1290 - wall_thickness (int, optional): <br> 1291 Thickness (in pixels) of detected walls (default: 0). 1292 - img_border_also_collide (bool, optional): <br> 1293 Whether image borders act as colliders (default: False). 1294 - reflexion_order (int, optional): <br> 1295 Number of allowed reflections per beam (default: 3). 1296 - should_scale_rays (bool, optional): <br> 1297 Whether to normalize ray coordinates to [0, 1] (default: True). 1298 - should_scale_img (bool, optional): <br> 1299 Whether to scale the input image before wall detection (default: True). 1300 - use_dda (bool, optional): <br> 1301 Whether to use the DDA-based ray tracing method (default: True). 1302 - iterative_tracking (bool, optional): 1303 Whether to return a RayIterator for step-by-step analysis (default: False). 1304 - iterative_steps (int, optional):<br> 1305 Number of steps for iterative reduction if using iterative tracking. `None` for all steps. 1306 - parallelization (int, optional):<br> 1307 The amount of workers for parallelization. 0 for no parallelization, -1 for max amount of workers. 1308 - parallelization_method (str, optional):<br> 1309 Method to use for parallelization (as soft condition) -> "threads" or "processes" 1310 - use_numba_compilation (bool, optional):<br> 1311 Whether to use the compiled (to machine code) version of compute heavy functions. 1312 - ignore_iterative_optimization (bool, optional): <br> 1313 Whether to used optimized ignore iteration data if iterative_tracking is False. 1314 1315 Returns: 1316 - list: <br> 1317 Nested list of traced beams and their reflection segments. 1318 Format: rays[beam][segment][point] = (x, y) 1319 """ 1320 if use_numba_compilation and iterative_tracking: 1321 print("[WARNING] Iterative Return deactivates the use of numba.") 1322 use_numba_compilation = False 1323 1324 if isinstance(img_src, np.ndarray): 1325 img = img_src 1326 else: 1327 img = img_open(src=img_src, should_scale=should_scale_img, should_print=False) 1328 IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 1329 abs_position = (rel_position[0] * IMG_WIDTH, rel_position[1] * IMG_HEIGHT) 1330 1331 if wall_values is not None and type(wall_values) not in [list, tuple]: 1332 wall_values = [wall_values] 1333 wall_map = get_wall_map(img=img, 1334 wall_values=wall_values, 1335 thickness=wall_thickness, 1336 use_numba_compilation=use_numba_compilation) 1337 1338 if wall_values is None: 1339 wall_values = [0.0] 1340 1341 # create function plan 1342 ray_planning = [] 1343 1344 for direction_in_degree in directions_in_degree: 1345 if use_dda: 1346 if use_numba_compilation: 1347 ray_tracing_func = trace_beam_with_DDA_numba 1348 else: 1349 ray_tracing_func = trace_beam_with_DDA 1350 else: 1351 if use_numba_compilation: 1352 ray_tracing_func = trace_beam_numba 1353 else: 1354 ray_tracing_func = trace_beam 1355 1356 ray_planning.append(lambda dir=direction_in_degree: ray_tracing_func( 1357 abs_position=abs_position, 1358 img=img, 1359 direction_in_degree=dir, 1360 wall_map=wall_map, 1361 wall_values=np.array(wall_values, dtype=np.float64) if (use_numba_compilation and use_dda) else wall_values, 1362 img_border_also_collide=img_border_also_collide, 1363 reflexion_order=reflexion_order, 1364 should_scale=should_scale_rays, 1365 should_return_iterative=iterative_tracking, 1366 remove_iterative=ignore_iterative_optimization 1367 ) 1368 ) 1369 1370 if iterative_tracking: 1371 rays = RayIterator() 1372 else: 1373 rays = [] 1374 1375 # compute 1376 if parallelization == 0: 1377 1378 for cur_ray_planning in ray_planning: 1379 cur_ray_result = cur_ray_planning() 1380 if use_numba_compilation: 1381 cur_ray_result = numba_to_py(cur_ray_result) 1382 1383 if iterative_tracking: 1384 rays.add_rays(cur_ray_result) 1385 else: 1386 rays.append(cur_ray_result) 1387 else: 1388 result_rays = Parallel(n_jobs=parallelization, 1389 # backend="threading" if use_numba_compilation else "loky", # process-based 1390 prefer="threads" if use_numba_compilation else parallelization_method, 1391 return_as="generator", 1392 batch_size=1 # because of unequal ray lengths 1393 )( 1394 delayed(ray_func)() for ray_func in ray_planning 1395 ) 1396 1397 for cur_ray_result in result_rays: 1398 if use_numba_compilation: 1399 cur_ray_result = numba_to_py(cur_ray_result) 1400 1401 if iterative_tracking: 1402 rays.add_rays(cur_ray_result) 1403 else: 1404 rays.append(cur_ray_result) 1405 1406 if iterative_tracking and iterative_steps is not None: 1407 rays.reduce_rays_iteratively(steps=iterative_steps) 1408 1409 return rays
Trace multiple rays (beams) from a single position through an image with walls and reflections.
Each beam starts from a given relative position and follows its assigned direction until it collides with a wall or image border. On collisions, reflections are computed based on local wall normals extracted from the image.
Parameters:
- rel_position (tuple):
Relative starting position (x, y) in normalized coordinates [0-1]. - img_src (str or numpy.ndarray):
Input image (array or file path) used for wall detection. - directions_in_degree (list):
List of beam direction angles (in degrees). - wall_values (list or float or None):
Pixel values representing walls or obstacles. - wall_thickness (int, optional):
Thickness (in pixels) of detected walls (default: 0). - img_border_also_collide (bool, optional):
Whether image borders act as colliders (default: False). - reflexion_order (int, optional):
Number of allowed reflections per beam (default: 3). - should_scale_rays (bool, optional):
Whether to normalize ray coordinates to [0, 1] (default: True). - should_scale_img (bool, optional):
Whether to scale the input image before wall detection (default: True). - use_dda (bool, optional):
Whether to use the DDA-based ray tracing method (default: True). - iterative_tracking (bool, optional): Whether to return a RayIterator for step-by-step analysis (default: False).
- iterative_steps (int, optional):
Number of steps for iterative reduction if using iterative tracking.Nonefor all steps. - parallelization (int, optional):
The amount of workers for parallelization. 0 for no parallelization, -1 for max amount of workers. - parallelization_method (str, optional):
Method to use for parallelization (as soft condition) -> "threads" or "processes" - use_numba_compilation (bool, optional):
Whether to use the compiled (to machine code) version of compute heavy functions. - ignore_iterative_optimization (bool, optional):
Whether to used optimized ignore iteration data if iterative_tracking is False.
Returns:
- list:
Nested list of traced beams and their reflection segments. Format: rays[beam][segment][point] = (x, y)
1413def scale_rays(rays, 1414 max_x=None, max_y=None, 1415 new_max_x=None, new_max_y=None, 1416 detailed_scaling=True): 1417 """ 1418 Scale ray coordinates between coordinate systems or image resolutions. 1419 1420 Optionally normalizes rays by old dimensions and rescales them to new ones. 1421 Can scale all points or just the start/end points of each beam. 1422 1423 Parameters: 1424 - rays (list): <br> 1425 Nested list of rays in the format rays[ray][beam][point] = (x, y). 1426 - max_x (float, optional): <br> 1427 Original maximum x-value for normalization. 1428 - max_y (float, optional): <br> 1429 Original maximum y-value for normalization. 1430 - new_max_x (float, optional): <br> 1431 New maximum x-value after rescaling. 1432 - new_max_y (float, optional): <br> 1433 New maximum y-value after rescaling. 1434 - detailed_scaling (bool, optional): <br> 1435 If True, scale every point in a beam; otherwise, only endpoints (default: True). 1436 1437 Returns: 1438 - list: <br> 1439 Scaled rays in the same nested format. 1440 """ 1441 if isinstance(rays, RayIterator): 1442 rays.apply_and_update(lambda r: scale_rays(r, max_x=max_x, max_y=max_y, new_max_x=new_max_x, new_max_y=new_max_y, detailed_scaling=detailed_scaling)) 1443 return rays 1444 1445 scaled_rays = [] 1446 for ray in rays: 1447 scaled_ray = [] 1448 for beams in ray: 1449 new_beams = copy.deepcopy(beams) 1450 if detailed_scaling: 1451 idx_to_process = list(range(len(beams))) 1452 else: 1453 idx_to_process = [0, len(beams)-1] 1454 1455 for idx in idx_to_process: 1456 x1, y1 = beams[idx] 1457 1458 if max_x is not None and max_y is not None: 1459 x1 /= max_x 1460 y1 /= max_y 1461 1462 from_cache = (x1, y1) 1463 if new_max_x is not None and new_max_y is not None: 1464 if x1 >= new_max_x/2: 1465 print(f"[WARNING] Detected high values scaling. Are you sure you want to scale for example a ray with {x1} to a value like {x1*new_max_x}?") 1466 if y1 >= new_max_y/2: 1467 print(f"[WARNING] Detected high values scaling. Are you sure you want to scale for example a ray with {y1} to a value like {y1*new_max_y}?") 1468 1469 x1 *= new_max_x 1470 y1 *= new_max_y 1471 1472 new_beams[idx] = (x1, y1) 1473 1474 scaled_ray.append(new_beams) 1475 scaled_rays.append(scaled_ray) 1476 return scaled_rays
Scale ray coordinates between coordinate systems or image resolutions.
Optionally normalizes rays by old dimensions and rescales them to new ones. Can scale all points or just the start/end points of each beam.
Parameters:
- rays (list):
Nested list of rays in the format rays[ray][beam][point] = (x, y). - max_x (float, optional):
Original maximum x-value for normalization. - max_y (float, optional):
Original maximum y-value for normalization. - new_max_x (float, optional):
New maximum x-value after rescaling. - new_max_y (float, optional):
New maximum y-value after rescaling. - detailed_scaling (bool, optional):
If True, scale every point in a beam; otherwise, only endpoints (default: True).
Returns:
- list:
Scaled rays in the same nested format.
1480def draw_rectangle_with_thickness(img, start_point, end_point, value, thickness=1): 1481 """ 1482 Draw a filled or thick rectangle on an image. 1483 1484 Expands the given start and end points based on the desired thickness and 1485 clips coordinates to image bounds to avoid overflow. 1486 1487 Parameters: 1488 - img (numpy.ndarray): <br> 1489 Image array to draw on. 1490 - start_point (tuple): <br> 1491 Top-left corner of the rectangle (x, y). 1492 - end_point (tuple): <br> 1493 Bottom-right corner of the rectangle (x, y). 1494 - value (int or float): <br> 1495 Fill value or color intensity. 1496 - thickness (int, optional): <br> 1497 Rectangle border thickness; 1498 if <= 0, the rectangle is filled (default: 1). 1499 """ 1500 # Calculate the expansion -> "thickness" 1501 if thickness > 0: 1502 expand = thickness // 2 1503 x1, y1 = start_point[0] - expand, start_point[1] - expand 1504 x2, y2 = end_point[0] + expand, end_point[1] + expand 1505 else: 1506 # thickness <= 0 → filled rectangle, no expansion 1507 x1, y1 = start_point 1508 x2, y2 = end_point 1509 1510 # Clip coordinates to image bounds 1511 x1 = max(0, x1) 1512 y1 = max(0, y1) 1513 x2 = min(img.shape[1]-1, x2) 1514 y2 = min(img.shape[0]-1, y2) 1515 1516 cv2.rectangle(img, (x1, y1), (x2, y2), value, thickness=-1)
Draw a filled or thick rectangle on an image.
Expands the given start and end points based on the desired thickness and clips coordinates to image bounds to avoid overflow.
Parameters:
- img (numpy.ndarray):
Image array to draw on. - start_point (tuple):
Top-left corner of the rectangle (x, y). - end_point (tuple):
Bottom-right corner of the rectangle (x, y). - value (int or float):
Fill value or color intensity. - thickness (int, optional):
Rectangle border thickness; if <= 0, the rectangle is filled (default: 1).
1520def draw_line_or_point(img, start_point, end_point, fill_value, thickness): 1521 """ 1522 Draw either a line or a single point on an image. 1523 1524 Determines whether to draw a point or a line based on whether the start and 1525 end coordinates are identical. 1526 1527 Parameters: 1528 - img (numpy.ndarray): <br> 1529 Image array to draw on. 1530 - start_point (tuple): <br> 1531 Starting pixel coordinate (x, y). 1532 - end_point (tuple): <br> 1533 Ending pixel coordinate (x, y). 1534 - fill_value (int or float): <br> 1535 Value or color used for drawing. 1536 - thickness (int): <br> 1537 Thickness of the line or point. 1538 """ 1539 draw_point = (start_point == end_point) 1540 1541 if draw_point: 1542 draw_rectangle_with_thickness(img=img, start_point=start_point, end_point=end_point, value=fill_value, thickness=thickness) 1543 else: 1544 cv2.line(img, start_point, end_point, fill_value, thickness)
Draw either a line or a single point on an image.
Determines whether to draw a point or a line based on whether the start and end coordinates are identical.
Parameters:
- img (numpy.ndarray):
Image array to draw on. - start_point (tuple):
Starting pixel coordinate (x, y). - end_point (tuple):
Ending pixel coordinate (x, y). - fill_value (int or float):
Value or color used for drawing. - thickness (int):
Thickness of the line or point.
1548def draw_rays(rays, detail_draw=True, 1549 output_format="single_image", # single_image, multiple_images, channels 1550 img_background=None, ray_value=255, ray_thickness=1, 1551 img_shape=(256, 256), dtype=float, standard_value=0, 1552 should_scale_rays_to_image=True, original_max_width=None, original_max_height=None, 1553 show_only_reflections=False): 1554 """ 1555 Render rays onto an image or a set of images. 1556 1557 Each ray can be drawn in full detail (every point) or as straight lines between 1558 beam endpoints. Rays can be scaled to match image dimensions and drawn on a 1559 single image, multiple images, or separate channels. 1560 1561 Parameters: 1562 - rays (list): <br> 1563 Nested list of rays in the format rays[ray][beam][point] = (x, y). 1564 - detail_draw (bool, optional): <br> 1565 Whether to draw every point or just beam endpoints (default: True). 1566 - output_format (str, optional): <br> 1567 Output mode: "single_image", "multiple_images", or "channels" (default: "single_image"). 1568 - img_background (numpy.ndarray, optional): <br> 1569 Background image; if None, a blank image is created. 1570 - ray_value (int, float, list, or numpy.ndarray, optional): <br> 1571 Pixel intensity or color per reflection (default: 255). 1572 - ray_thickness (int, optional): <br> 1573 Thickness of the drawn lines or points (default: 1). 1574 - img_shape (tuple, optional): <br> 1575 Shape of the generated image if no background is given (default: (256, 256)). 1576 - dtype (type, optional): <br> 1577 Data type for the output image (default: float). 1578 - standard_value (int or float, optional): <br> 1579 Background fill value (default: 0). 1580 - should_scale_rays_to_image (bool, optional): <br> 1581 Whether to scale ray coordinates to match the image (default: True). 1582 - original_max_width (float, optional): <br> 1583 Original image width before scaling. 1584 - original_max_height (float, optional): <br> 1585 Original image height before scaling. 1586 - show_only_reflections (bool, optional): <br> 1587 If True, draws only reflected beams (default: False). 1588 1589 Returns: 1590 numpy.ndarray or list: <br> 1591 - Single image if output_format == "single_image" or "channels". 1592 - List of images if output_format == "multiple_images". 1593 """ 1594 if isinstance(rays, RayIterator): 1595 imgs = rays.apply_and_return(lambda r: draw_rays(r, detail_draw=detail_draw, 1596 output_format=output_format, 1597 img_background=img_background, 1598 ray_value=ray_value, 1599 ray_thickness=ray_thickness, 1600 img_shape=img_shape, 1601 dtype=dtype, 1602 standard_value=standard_value, 1603 should_scale_rays_to_image=should_scale_rays_to_image, 1604 original_max_width=original_max_width, 1605 original_max_height=original_max_height, 1606 show_only_reflections=show_only_reflections)) 1607 return imgs 1608 1609 # prepare background image 1610 if img_background is None: 1611 img = np.full(shape=img_shape, fill_value=dtype(standard_value), dtype=dtype) 1612 else: 1613 img = img_background.copy() 1614 1615 # rescale rays to fit inside image bounds if desired 1616 height, width = img.shape[:2] 1617 # print(f"{height, width=}") 1618 # print(f"{(original_max_width, original_max_height)=}") 1619 if should_scale_rays_to_image: 1620 rays = scale_rays(rays, max_x=original_max_width, max_y=original_max_height, new_max_x=width-1, new_max_y=height-1, detailed_scaling=detail_draw) 1621 1622 nrays = len(rays) 1623 if output_format == "channels": 1624 img = np.repeat(img[..., None], nrays, axis=-1) 1625 # img_shape += (nrays, ) 1626 # img = np.full(shape=img_shape, fill_value=dtype(standard_value), dtype=dtype) 1627 elif output_format == "multiple_images": 1628 imgs = [np.copy(img) for _ in range(nrays)] 1629 1630 # draw on image 1631 for idx, ray in enumerate(rays): 1632 for reflexion_order, beam_points in enumerate(ray): 1633 1634 if detail_draw: 1635 lines = [] 1636 for cur_point in range(0, len(beam_points)): 1637 start_point = tuple(map(lambda x:int(x), beam_points[cur_point])) 1638 end_point = tuple(map(lambda x:int(x), beam_points[cur_point])) 1639 # end_point = tuple(map(lambda x:int(x), beam_points[cur_point+1])) 1640 # -> if as small lines then in range: range(0, len(beam_points)-1) 1641 lines.append((start_point, end_point, reflexion_order)) 1642 else: 1643 start_point = tuple(map(lambda x:int(x), beam_points[0])) 1644 end_point = tuple(map(lambda x:int(x), beam_points[-1])) 1645 lines = [(start_point, end_point, reflexion_order)] 1646 1647 for start_point, end_point, reflexion_order in lines: 1648 1649 if show_only_reflections and reflexion_order == 0: 1650 continue 1651 1652 # get cur ray value 1653 if type(ray_value) in [list, tuple, np.ndarray]: 1654 # if we print without the first line: first value (index 0) belkongs to the reflexion order 1 1655 cur_reflexion_index = reflexion_order-1 if show_only_reflections else reflexion_order 1656 cur_ray_value = ray_value[min(len(ray_value)-1, cur_reflexion_index)] 1657 else: 1658 cur_ray_value = ray_value 1659 1660 if output_format == "channels": 1661 layer = np.ascontiguousarray(img[..., idx]) 1662 draw_line_or_point(img=layer, start_point=start_point, end_point=end_point, fill_value=cur_ray_value, thickness=ray_thickness) 1663 img[..., idx] = layer 1664 elif output_format == "multiple_images": 1665 draw_line_or_point(img=imgs[idx], start_point=start_point, end_point=end_point, fill_value=cur_ray_value, thickness=ray_thickness) 1666 else: 1667 draw_line_or_point(img=img, start_point=start_point, end_point=end_point, fill_value=cur_ray_value, thickness=ray_thickness) 1668 1669 1670 if output_format == "multiple_images": 1671 return imgs 1672 else: 1673 return img
Render rays onto an image or a set of images.
Each ray can be drawn in full detail (every point) or as straight lines between beam endpoints. Rays can be scaled to match image dimensions and drawn on a single image, multiple images, or separate channels.
Parameters:
- rays (list):
Nested list of rays in the format rays[ray][beam][point] = (x, y). - detail_draw (bool, optional):
Whether to draw every point or just beam endpoints (default: True). - output_format (str, optional):
Output mode: "single_image", "multiple_images", or "channels" (default: "single_image"). - img_background (numpy.ndarray, optional):
Background image; if None, a blank image is created. - ray_value (int, float, list, or numpy.ndarray, optional):
Pixel intensity or color per reflection (default: 255). - ray_thickness (int, optional):
Thickness of the drawn lines or points (default: 1). - img_shape (tuple, optional):
Shape of the generated image if no background is given (default: (256, 256)). - dtype (type, optional):
Data type for the output image (default: float). - standard_value (int or float, optional):
Background fill value (default: 0). - should_scale_rays_to_image (bool, optional):
Whether to scale ray coordinates to match the image (default: True). - original_max_width (float, optional):
Original image width before scaling. - original_max_height (float, optional):
Original image height before scaling. - show_only_reflections (bool, optional):
If True, draws only reflected beams (default: False).
Returns:
numpy.ndarray or list:
- Single image if output_format == "single_image" or "channels".
- List of images if output_format == "multiple_images".
1677def trace_and_draw_rays(rel_position, 1678 img_src, 1679 directions_in_degree, 1680 wall_values, 1681 wall_thickness=0, 1682 img_border_also_collide=False, 1683 reflexion_order=3, 1684 should_scale_rays=True, 1685 should_scale_img=True, 1686 use_dda=True, 1687 iterative_tracking=False, 1688 iterative_steps=None, 1689 parallelization=-1, 1690 parallelization_method="processes", # "threads", "processes" 1691 use_numba_compilation=True, 1692 ignore_iterative_optimization=True, 1693 detail_draw=True, 1694 output_format="single_image", # single_image, multiple_images, channels 1695 img_background=None, 1696 ray_value=255, 1697 ray_thickness=1, 1698 img_shape=(256, 256), 1699 dtype=float, 1700 standard_value=0, 1701 should_scale_rays_to_image=True, 1702 original_max_width=None, 1703 original_max_height=None, 1704 show_only_reflections=False): 1705 """ 1706 Trace multiple rays (beams) from a single position through an image with walls and reflections AND 1707 render rays onto an image or a set of images. 1708 1709 Calls internally `trace_beams` and then `draw_rays`. 1710 1711 Parameters: 1712 - rel_position (tuple): <br> 1713 Relative starting position (x, y) in normalized coordinates [0-1]. 1714 - img_src (str or numpy.ndarray): <br> 1715 Input image (array or file path) used for wall detection. 1716 - directions_in_degree (list): <br> 1717 List of beam direction angles (in degrees). 1718 - wall_values (list or float or None): <br> 1719 Pixel values representing walls or obstacles. 1720 - wall_thickness (int, optional): <br> 1721 Thickness (in pixels) of detected walls (default: 0). 1722 - img_border_also_collide (bool, optional): <br> 1723 Whether image borders act as colliders (default: False). 1724 - reflexion_order (int, optional): <br> 1725 Number of allowed reflections per beam (default: 3). 1726 - should_scale_rays (bool, optional): <br> 1727 Whether to normalize ray coordinates to [0, 1] (default: True). 1728 - should_scale_img (bool, optional): <br> 1729 Whether to scale the input image before wall detection (default: True). 1730 - use_dda (bool, optional): <br> 1731 Whether to use the DDA-based ray tracing method (default: True). 1732 - iterative_tracking (bool, optional): 1733 Whether to return a RayIterator for step-by-step analysis (default: False). 1734 - iterative_steps (int, optional):<br> 1735 Number of steps for iterative reduction if using iterative tracking. `None` for all steps. 1736 - parallelization (int, optional):<br> 1737 The amount of workers for parallelization. 0 for no parallelization, -1 for max amount of workers. 1738 - parallelization_method (str, optional):<br> 1739 Method to use for parallelization (as soft condition) -> "threads" or "processes" 1740 - use_numba_compilation (bool, optional):<br> 1741 Whether to use the compiled (to machine code) version of compute heavy functions. 1742 - ignore_iterative_optimization (bool, optional): <br> 1743 Whether to used optimized ignore iteration data if iterative_tracking is False. 1744 - detail_draw (bool, optional): <br> 1745 Whether to draw every point or just beam endpoints (default: True). 1746 - output_format (str, optional): <br> 1747 Output mode: "single_image", "multiple_images", or "channels" (default: "single_image"). 1748 - img_background (numpy.ndarray, optional): <br> 1749 Background image; if None, a blank image is created. 1750 - ray_value (int, float, list, or numpy.ndarray, optional): <br> 1751 Pixel intensity or color per reflection (default: 255). 1752 - ray_thickness (int, optional): <br> 1753 Thickness of the drawn lines or points (default: 1). 1754 - img_shape (tuple, optional): <br> 1755 Shape of the generated image if no background is given (default: (256, 256)). 1756 - dtype (type, optional): <br> 1757 Data type for the output image (default: float). 1758 - standard_value (int or float, optional): <br> 1759 Background fill value (default: 0). 1760 - should_scale_rays_to_image (bool, optional): <br> 1761 Whether to scale ray coordinates to match the image (default: True). 1762 - original_max_width (float, optional): <br> 1763 Original image width before scaling. 1764 - original_max_height (float, optional): <br> 1765 Original image height before scaling. 1766 - show_only_reflections (bool, optional): <br> 1767 If True, draws only reflected beams (default: False). 1768 1769 Returns: 1770 numpy.ndarray or list: 1771 - Single image if output_format == "single_image" or "channels". 1772 - List of images if output_format == "multiple_images". 1773 """ 1774 rays = trace_beams(rel_position, 1775 img_src, 1776 directions_in_degree, 1777 wall_values, 1778 wall_thickness=wall_thickness, 1779 img_border_also_collide=img_border_also_collide, 1780 reflexion_order=reflexion_order, 1781 should_scale_rays=should_scale_rays, 1782 should_scale_img=should_scale_img, 1783 use_dda=use_dda, 1784 iterative_tracking=iterative_tracking, 1785 iterative_steps=iterative_steps, 1786 parallelization=parallelization, 1787 parallelization_method=parallelization_method, 1788 use_numba_compilation=use_numba_compilation, 1789 ignore_iterative_optimization=ignore_iterative_optimization) 1790 1791 return draw_rays(rays, 1792 detail_draw=detail_draw, 1793 output_format=output_format, 1794 img_background=img_background, 1795 ray_value=ray_value, 1796 ray_thickness=ray_thickness, 1797 img_shape=img_shape, 1798 dtype=dtype, 1799 standard_value=standard_value, 1800 should_scale_rays_to_image=should_scale_rays_to_image, 1801 original_max_width=original_max_width, 1802 original_max_height=original_max_height, 1803 show_only_reflections=show_only_reflections)
Trace multiple rays (beams) from a single position through an image with walls and reflections AND render rays onto an image or a set of images.
Calls internally trace_beams and then draw_rays.
Parameters:
- rel_position (tuple):
Relative starting position (x, y) in normalized coordinates [0-1]. - img_src (str or numpy.ndarray):
Input image (array or file path) used for wall detection. - directions_in_degree (list):
List of beam direction angles (in degrees). - wall_values (list or float or None):
Pixel values representing walls or obstacles. - wall_thickness (int, optional):
Thickness (in pixels) of detected walls (default: 0). - img_border_also_collide (bool, optional):
Whether image borders act as colliders (default: False). - reflexion_order (int, optional):
Number of allowed reflections per beam (default: 3). - should_scale_rays (bool, optional):
Whether to normalize ray coordinates to [0, 1] (default: True). - should_scale_img (bool, optional):
Whether to scale the input image before wall detection (default: True). - use_dda (bool, optional):
Whether to use the DDA-based ray tracing method (default: True). - iterative_tracking (bool, optional): Whether to return a RayIterator for step-by-step analysis (default: False).
- iterative_steps (int, optional):
Number of steps for iterative reduction if using iterative tracking.Nonefor all steps. - parallelization (int, optional):
The amount of workers for parallelization. 0 for no parallelization, -1 for max amount of workers. - parallelization_method (str, optional):
Method to use for parallelization (as soft condition) -> "threads" or "processes" - use_numba_compilation (bool, optional):
Whether to use the compiled (to machine code) version of compute heavy functions. - ignore_iterative_optimization (bool, optional):
Whether to used optimized ignore iteration data if iterative_tracking is False. - detail_draw (bool, optional):
Whether to draw every point or just beam endpoints (default: True). - output_format (str, optional):
Output mode: "single_image", "multiple_images", or "channels" (default: "single_image"). - img_background (numpy.ndarray, optional):
Background image; if None, a blank image is created. - ray_value (int, float, list, or numpy.ndarray, optional):
Pixel intensity or color per reflection (default: 255). - ray_thickness (int, optional):
Thickness of the drawn lines or points (default: 1). - img_shape (tuple, optional):
Shape of the generated image if no background is given (default: (256, 256)). - dtype (type, optional):
Data type for the output image (default: float). - standard_value (int or float, optional):
Background fill value (default: 0). - should_scale_rays_to_image (bool, optional):
Whether to scale ray coordinates to match the image (default: True). - original_max_width (float, optional):
Original image width before scaling. - original_max_height (float, optional):
Original image height before scaling. - show_only_reflections (bool, optional):
If True, draws only reflected beams (default: False).
Returns: numpy.ndarray or list: - Single image if output_format == "single_image" or "channels". - List of images if output_format == "multiple_images".
1810def numba_to_py(obj): 1811 """ 1812 Converts numba.typed.List (possibly nested -> rays) to plain Python lists/tuples. 1813 """ 1814 if isinstance(obj, List): 1815 return [numba_to_py(x) for x in obj] 1816 1817 1818 if isinstance(obj, tuple): 1819 return tuple(numba_to_py(x) for x in obj) 1820 1821 return obj
Converts numba.typed.List (possibly nested -> rays) to plain Python lists/tuples.
789def calc_reflection(collide_vector, wall_vector): 790 """ 791 Calculate the reflection of a collision vector against a wall vector. 792 793 The reflection is computed using the wall's normal vector and the formula: 794 r = v - 2 * (v · n) * n 795 796 Parameters: 797 - collide_vector (array-like): <br> 798 Incoming vector (2D). 799 - wall_vector (array-like): <br> 800 Wall direction vector (2D). 801 802 Returns: 803 - numpy.ndarray: 804 Reflected 2D vector. 805 """ 806 # normalize both 807 collide_vector = np.array(collide_vector, dtype=np.float64) 808 collide_vector /= np.linalg.norm(collide_vector) 809 wall_vector = np.array(wall_vector, dtype=np.float64) 810 wall_vector /= np.linalg.norm(wall_vector) 811 812 # calculate the normal of the wall 813 normal_wall_vector_1 = np.array([-wall_vector[1], wall_vector[0]]) # rotated +90° 814 normal_wall_vector_2 = np.array([wall_vector[1], -wall_vector[0]]) # rotated -90° 815 816 # decide which vector is the right one 817 # -> dot product tells which normal faces the incoming vector 818 # -> dor product shows how similiar 2 vectors are => smaller 0 means they show against each other => right vector 819 if np.dot(collide_vector, normal_wall_vector_1) < 0: 820 normal_wall_vector = normal_wall_vector_1 821 else: 822 normal_wall_vector = normal_wall_vector_2 823 824 # calc the reflection 825 return collide_vector - 2 * np.dot(collide_vector, normal_wall_vector) * normal_wall_vector
Calculate the reflection of a collision vector against a wall vector.
The reflection is computed using the wall's normal vector and the formula: r = v - 2 * (v · n) * n
Parameters:
- collide_vector (array-like):
Incoming vector (2D). - wall_vector (array-like):
Wall direction vector (2D).
Returns:
- numpy.ndarray: Reflected 2D vector.
828def get_img_border_vector(position, max_width, max_height): 829 """ 830 Determine the wall normal vector for an image border collision. 831 832 Parameters: 833 - position (tuple): <br> 834 Current position (x, y). 835 - max_width (int): <br> 836 Image width. 837 - max_height (int): <br> 838 Image height. 839 840 Returns: 841 - tuple: 842 Border wall vector corresponding to the collision side. 843 """ 844 # print(f"got {position=}") 845 if position[0] < 0: 846 return (0, 1) 847 elif position[0] >= max_width: 848 return (0, 1) 849 elif position[1] < 0: 850 return (1, 0) 851 elif position[1] >= max_height: 852 return (1, 0) 853 else: 854 # should never reach that! 855 return (0, 0)
Determine the wall normal vector for an image border collision.
Parameters:
- position (tuple):
Current position (x, y). - max_width (int):
Image width. - max_height (int):
Image height.
Returns:
- tuple: Border wall vector corresponding to the collision side.
1837@numba.njit(cache=True, fastmath=True) 1838def update_pixel_position_numba(direction_in_degree, cur_position, target_line): 1839 # cur_position: (x,y) tuple 1840 # target_line: array[4] => [x1,y1,x2,y2] 1841 px = cur_position[0] 1842 py = cur_position[1] 1843 1844 ax = target_line[0] 1845 ay = target_line[1] 1846 bx = target_line[2] 1847 by = target_line[3] 1848 1849 # Directionvector of the line 1850 vx = bx - ax 1851 vy = by - ay 1852 1853 # Projection: t = dot(P-A, V) / dot(V,V) 1854 wx = px - ax 1855 wy = py - ay 1856 1857 denom = vx*vx + vy*vy 1858 if denom == 0.0: 1859 # Degenerated Linie: just no movement 1860 return (px, py) 1861 1862 t = (wx*vx + wy*vy) / denom 1863 1864 # clip scalar (Numba-safe) 1865 if t < 0.0: 1866 t = 0.0 1867 elif t > 1.0: 1868 t = 1.0 1869 1870 # Point on the line 1871 lx = ax + t * vx 1872 ly = ay + t * vy 1873 1874 # Now make a step in direction_in_degree 1875 rad = math.radians(direction_in_degree) 1876 dx = math.cos(rad) 1877 dy = math.sin(rad) 1878 1879 nx = lx + dx 1880 ny = ly + dy 1881 1882 return (nx, ny)
1940@numba.njit(cache=True, fastmath=True) 1941def trace_beam_numba(abs_position, 1942 img, 1943 direction_in_degree, 1944 wall_map, 1945 wall_values, 1946 img_border_also_collide=False, 1947 reflexion_order=3, 1948 should_scale=True, 1949 should_return_iterative=False, 1950 remove_iterative=True): 1951 """ 1952 Trace a ray (beam) through an image with walls and reflections. 1953 1954 The beam starts from a given position and follows a direction until it hits 1955 a wall or border. On collisions, reflections are computed using wall normals. 1956 1957 Parameters: 1958 - abs_position (tuple): <br> 1959 Starting position (x, y) of the beam. 1960 - img (numpy.ndarray): <br> 1961 Input image or segmentation map. 1962 - direction_in_degree (float): <br> 1963 Initial direction angle of the beam. 1964 - wall_map (numpy.ndarray): <br> 1965 Map containing wall orientations in degrees. 1966 - wall_values (list): <br> 1967 List of pixel values representing walls. 1968 - img_border_also_collide (bool, optional): <br> 1969 Whether the image border acts as a collider (default: False). 1970 - reflexion_order (int, optional): <br> 1971 Number of allowed reflections (default: 3). 1972 - should_scale (bool, optional): <br> 1973 Whether to normalize positions to [0, 1] (default: True). 1974 - should_return_iterative (bool, optional): <br> 1975 Whether to return a RayIterator for step-by-step analysis (default: False). 1976 - ignore_iterative_optimization (bool, optional): <br> 1977 Whether to used optimized ignore iteration data if iterative_tracking is False. 1978 - remove_iterative (bool, optional): <br> 1979 Ignored in this numba version. 1980 1981 Returns: 1982 - list: 1983 Nested list structure representing the traced ray and its reflections. 1984 Format: ray[beam][point] = (x, y) 1985 """ 1986 if should_return_iterative: 1987 print("[WARNING] Numba Version can't return a iterative version.") 1988 1989 reflexion_order += 1 1990 IMG_HEIGHT = img.shape[0] 1991 IMG_WIDTH = img.shape[1] 1992 1993 ray = List.empty_list(TYPE_LINE) 1994 1995 cur_target_abs_position = (float(abs_position[0]), float(abs_position[1])) 1996 cur_direction_in_degree = float(direction_in_degree % 360.0) 1997 1998 w_f = float(IMG_WIDTH) 1999 h_f = float(IMG_HEIGHT) 2000 2001 inf_val = np.inf 2002 2003 for _ in range(reflexion_order): 2004 # current_ray_line als typed list 2005 current_ray_line = List.empty_list(TYPE_POINT_FLOAT) 2006 2007 if should_scale: 2008 p = normalize_point_numba(x=cur_target_abs_position[0], 2009 y=cur_target_abs_position[1], 2010 width=IMG_WIDTH, 2011 height=IMG_HEIGHT) 2012 current_ray_line.append((float(p[0]), float(p[1]))) 2013 else: 2014 current_ray_line.append((cur_target_abs_position[0], cur_target_abs_position[1])) 2015 2016 last_abs_position = cur_target_abs_position 2017 2018 # Direction -> dx,dy 2019 rad = math.radians(cur_direction_in_degree) 2020 dx = math.cos(rad) 2021 dy = math.sin(rad) 2022 2023 # target_line 2024 x1, y1, x2, y2 = _compute_target_line_outside(cur_target_abs_position[0], 2025 cur_target_abs_position[1], 2026 dx, dy, w_f, h_f) 2027 # build target line -> start and end point 2028 target_line = np.empty(4, dtype=np.float64) 2029 target_line[0] = x1 2030 target_line[1] = y1 2031 target_line[2] = x2 2032 target_line[3] = y2 2033 2034 current_position = cur_target_abs_position 2035 2036 while True: 2037 current_position = update_pixel_position_numba( 2038 direction_in_degree=cur_direction_in_degree, 2039 cur_position=current_position, 2040 target_line=target_line 2041 ) 2042 2043 x = current_position[0] 2044 y = current_position[1] 2045 2046 # Border check 2047 if not (0.0 <= x < w_f and 0.0 <= y < h_f): 2048 ray.append(current_ray_line) 2049 2050 if img_border_also_collide: 2051 wall_vector = get_img_border_vector_numba( 2052 position=current_position, 2053 max_width=IMG_WIDTH, 2054 max_height=IMG_HEIGHT 2055 ) 2056 new_direction = calc_reflection_numba( 2057 collide_vector=degree_to_vector_numba(cur_direction_in_degree), 2058 wall_vector=wall_vector 2059 ) 2060 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2061 2062 cur_target_abs_position = last_abs_position 2063 cur_direction_in_degree = float(new_direction_in_degree) 2064 break 2065 else: 2066 return ray 2067 2068 ix = int(x) 2069 iy = int(y) 2070 2071 next_pixel = float(img[iy, ix]) 2072 2073 # Wall check (Numba-save) 2074 if _is_in_wall_values(next_pixel, wall_values): 2075 last_abs_position = (x, y) 2076 ray.append(current_ray_line) 2077 2078 building_angle = wall_map[iy, ix] 2079 if building_angle == inf_val: 2080 raise Exception("Got inf value from Wall-Map.") 2081 2082 wall_vector = degree_to_vector_numba(building_angle) 2083 2084 new_direction = calc_reflection_numba( 2085 collide_vector=degree_to_vector_numba(cur_direction_in_degree), 2086 wall_vector=wall_vector 2087 ) 2088 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2089 2090 cur_target_abs_position = last_abs_position 2091 cur_direction_in_degree = float(new_direction_in_degree) 2092 break 2093 else: 2094 if should_scale: 2095 p = normalize_point_numba(x=x, y=y, width=IMG_WIDTH, height=IMG_HEIGHT) 2096 current_ray_line.append((float(p[0]), float(p[1]))) 2097 else: 2098 current_ray_line.append((x, y)) 2099 last_abs_position = (x, y) 2100 2101 return ray
Trace a ray (beam) through an image with walls and reflections.
The beam starts from a given position and follows a direction until it hits a wall or border. On collisions, reflections are computed using wall normals.
Parameters:
- abs_position (tuple):
Starting position (x, y) of the beam. - img (numpy.ndarray):
Input image or segmentation map. - direction_in_degree (float):
Initial direction angle of the beam. - wall_map (numpy.ndarray):
Map containing wall orientations in degrees. - wall_values (list):
List of pixel values representing walls. - img_border_also_collide (bool, optional):
Whether the image border acts as a collider (default: False). - reflexion_order (int, optional):
Number of allowed reflections (default: 3). - should_scale (bool, optional):
Whether to normalize positions to [0, 1] (default: True). - should_return_iterative (bool, optional):
Whether to return a RayIterator for step-by-step analysis (default: False). - ignore_iterative_optimization (bool, optional):
Whether to used optimized ignore iteration data if iterative_tracking is False. - remove_iterative (bool, optional):
Ignored in this numba version.
Returns:
- list: Nested list structure representing the traced ray and its reflections. Format: ray[beam][point] = (x, y)
2105@numba.njit(cache=True, fastmath=True) 2106def trace_beam_with_DDA_numba(abs_position, 2107 img, 2108 direction_in_degree, 2109 wall_map, 2110 wall_values, 2111 img_border_also_collide=False, 2112 reflexion_order=3, 2113 should_scale=True, 2114 should_return_iterative=False, 2115 remove_iterative=True): 2116 """ 2117 Trace a ray (beam) through a 2D image using a DDA (Digital Differential Analyzer) 2118 algorithm with precise collision points and physically accurate reflections. 2119 2120 The beam starts at a given floating-point position and marches through the grid 2121 until it intersects a wall or exits the image. For each collision, the exact 2122 hit position is computed using the ray Parameters t_hit, ensuring that reflected 2123 segments contain meaningful geometry rather than single-point artifacts. 2124 Reflections are computed using wall normals derived from the `wall_map`. 2125 2126 Parameters: 2127 - abs_position (tuple of float): <br> 2128 Starting position (x, y) of the beam in absolute pixel space. 2129 - img (numpy.ndarray): <br> 2130 2D array representing the scene. Pixel values listed in `wall_values` 2131 are treated as solid walls. 2132 - direction_in_degree (float): <br> 2133 Initial direction of the beam in degrees (0° = right, 90° = down). 2134 - wall_map (numpy.ndarray): <br> 2135 A map storing wall orientations in degrees for each pixel marked as a wall. 2136 These angles define the wall normals used for reflection. 2137 - wall_values (list, tuple, set, float, optional): <br> 2138 Pixel values identifying walls. Any pixel in this list causes a collision. 2139 If None, pixel value 0.0 is treated as a wall. 2140 - img_border_also_collide (bool, optional): <br> 2141 If True, the image borders behave like reflective walls. If False, 2142 the ray terminates when leaving the image. Default: False. 2143 - reflexion_order (int, optional): <br> 2144 Maximum number of reflections. The ray can rebound this many times before 2145 the function terminates. Default: 3. 2146 - should_scale (bool, optional): <br> 2147 If True, all emitted points (x, y) are normalized to [0, 1] range. 2148 Otherwise absolute pixel positions are returned. Default: True. 2149 - should_return_iterative (bool, optional): <br> 2150 Whether to return a RayIterator for step-by-step analysis (default: False). 2151 - remove_iterative (bool, optional): <br> 2152 Ignored in this numba version. 2153 2154 Returns: 2155 - list: 2156 Nested list structure representing the traced ray and its reflections. 2157 Format: ray[beam][point] = (x, y) 2158 """ 2159 if should_return_iterative: 2160 print("[WARNING] Numba Version can't return a iterative version.") 2161 2162 reflexion_order += 1 2163 # IMG_WIDTH, IMG_HEIGHT = get_width_height(img) 2164 IMG_HEIGHT = img.shape[0] 2165 IMG_WIDTH = img.shape[1] 2166 2167 ray = [] 2168 2169 cur_target_abs_position = (float(abs_position[0]), float(abs_position[1])) 2170 cur_direction_in_degree = direction_in_degree % 360 2171 2172 # go through every reflection -> will early stop if hitting a wall (if wall-bouncing is deactivated) 2173 for cur_depth in range(reflexion_order): 2174 if should_scale: 2175 current_ray_line = [normalize_point_numba(x=cur_target_abs_position[0], y=cur_target_abs_position[1], width=IMG_WIDTH, height=IMG_HEIGHT)] 2176 else: 2177 current_ray_line = [(cur_target_abs_position[0], cur_target_abs_position[1])] 2178 2179 last_abs_position = cur_target_abs_position 2180 2181 # direction 2182 rad = math.radians(cur_direction_in_degree) 2183 dx = math.cos(rad) 2184 dy = math.sin(rad) 2185 2186 eps = 1e-12 2187 if abs(dx) < eps: dx = 0.0 2188 if abs(dy) < eps: dy = 0.0 2189 2190 # start float pos and starting cell 2191 x0 = float(cur_target_abs_position[0]) 2192 y0 = float(cur_target_abs_position[1]) 2193 cell_x = int(math.floor(x0)) 2194 cell_y = int(math.floor(y0)) 2195 2196 # outside start -> handle border/reflection/exit 2197 if not (0 <= x0 < IMG_WIDTH and 0 <= y0 < IMG_HEIGHT): 2198 ray.append(current_ray_line) 2199 if img_border_also_collide: 2200 wall_vector = get_img_border_vector_numba(position=(x0, y0), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 2201 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2202 cur_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2203 cur_target_abs_position = last_abs_position 2204 continue 2205 else: 2206 return ray 2207 2208 # DDA parameters 2209 tDeltaX = math.inf if dx == 0.0 else abs(1.0 / dx) 2210 tDeltaY = math.inf if dy == 0.0 else abs(1.0 / dy) 2211 2212 if dx > 0: 2213 stepX = 1 2214 nextBoundaryX = cell_x + 1.0 2215 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 2216 elif dx < 0: 2217 stepX = -1 2218 nextBoundaryX = cell_x * 1.0 # left boundary of cell 2219 tMaxX = (nextBoundaryX - x0) / dx if dx != 0 else math.inf 2220 else: 2221 stepX = 0 2222 tMaxX = math.inf 2223 2224 if dy > 0: 2225 stepY = 1 2226 nextBoundaryY = cell_y + 1.0 2227 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 2228 elif dy < 0: 2229 stepY = -1 2230 nextBoundaryY = cell_y * 1.0 2231 tMaxY = (nextBoundaryY - y0) / dy if dy != 0 else math.inf 2232 else: 2233 stepY = 0 2234 tMaxY = math.inf 2235 2236 max_steps = (IMG_WIDTH + IMG_HEIGHT) * 6 2237 steps = 0 2238 last_position_saved = False 2239 2240 # immediate-start-in-wall handling 2241 if 0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT: 2242 start_pixel = float(img[cell_y, cell_x]) 2243 # if start_pixel in wall_values_set: 2244 if _is_in_wall_values(start_pixel, wall_values): 2245 # compute a collision point precisely at start (we'll use origin) 2246 # add collision point (start) and reflect 2247 hit_x = x0 2248 hit_y = y0 2249 if should_scale: 2250 current_ray_line.append(normalize_point_numba(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)) 2251 else: 2252 current_ray_line.append((hit_x, hit_y)) 2253 ray.append(current_ray_line) 2254 2255 building_angle = float(wall_map[cell_y, cell_x]) 2256 if not np.isfinite(building_angle): 2257 raise Exception("Got non-finite value from Wall-Map.") 2258 wall_vector = degree_to_vector_numba(building_angle) 2259 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2260 cur_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2261 ndx, ndy = new_direction[0], new_direction[1] 2262 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 2263 continue 2264 2265 # DDA main loop 2266 while steps < max_steps: 2267 steps += 1 2268 2269 # choose axis to step and capture t_hit (distance along ray to boundary) 2270 if tMaxX < tMaxY: 2271 t_hit = tMaxX 2272 # step in x 2273 cell_x += stepX 2274 tMaxX += tDeltaX 2275 stepped_axis = 'x' 2276 else: 2277 t_hit = tMaxY 2278 # step in y 2279 cell_y += stepY 2280 tMaxY += tDeltaY 2281 stepped_axis = 'y' 2282 2283 # compute exact collision position along ray from origin (x0,y0) 2284 hit_x = x0 + dx * t_hit 2285 hit_y = y0 + dy * t_hit 2286 2287 # For recording the traversal we can append intermediate cell centers encountered so far. 2288 # But more importantly, append the collision point to the current segment BEFORE storing it. 2289 if should_scale: 2290 current_ray_line.append(normalize_point_numba(x=hit_x, y=hit_y, width=IMG_WIDTH, height=IMG_HEIGHT)) 2291 else: 2292 current_ray_line.append((hit_x, hit_y)) 2293 2294 # Now check if we've left the image bounds (cell_x, cell_y refer to the new cell we stepped into) 2295 if not (0 <= cell_x < IMG_WIDTH and 0 <= cell_y < IMG_HEIGHT): 2296 ray.append(current_ray_line) 2297 last_position_saved = True 2298 2299 if img_border_also_collide: 2300 wall_vector = get_img_border_vector_numba(position=(cell_x, cell_y), max_width=IMG_WIDTH, max_height=IMG_HEIGHT) 2301 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2302 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2303 # start next ray from last in-image position (hit_x, hit_y) nudged slightly 2304 ndx, ndy = new_direction[0], new_direction[1] 2305 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 2306 cur_direction_in_degree = new_direction_in_degree 2307 break 2308 else: 2309 return ray 2310 2311 # sample the pixel in the cell we stepped into 2312 next_pixel = float(img[cell_y, cell_x]) 2313 if _is_in_wall_values(next_pixel, wall_values): 2314 # we hit a wall — collision point already appended 2315 last_abs_position = (hit_x, hit_y) 2316 ray.append(current_ray_line) 2317 last_position_saved = True 2318 2319 building_angle = float(wall_map[cell_y, cell_x]) 2320 if not np.isfinite(building_angle): 2321 raise Exception("Got non-finite value from Wall-Map.") 2322 wall_vector = degree_to_vector_numba(building_angle) 2323 2324 new_direction = calc_reflection_numba(collide_vector=degree_to_vector_numba(cur_direction_in_degree), wall_vector=wall_vector) 2325 new_direction_in_degree = vector_to_degree_numba(new_direction[0], new_direction[1]) 2326 2327 # start next beam from collision point nudged outwards 2328 ndx, ndy = new_direction[0], new_direction[1] 2329 cur_target_abs_position = (hit_x + ndx * 1e-3, hit_y + ndy * 1e-3) 2330 cur_direction_in_degree = new_direction_in_degree 2331 break 2332 else: 2333 # no hit -> continue marching; also add a representative point in the traversed cell (optional) 2334 # we already appended the exact hit point for this step; for smoother lines you may add cell center too 2335 last_abs_position = (hit_x, hit_y) 2336 # continue 2337 2338 # end DDA loop 2339 if not last_position_saved: 2340 ray.append(current_ray_line) 2341 2342 return ray
Trace a ray (beam) through a 2D image using a DDA (Digital Differential Analyzer) algorithm with precise collision points and physically accurate reflections.
The beam starts at a given floating-point position and marches through the grid
until it intersects a wall or exits the image. For each collision, the exact
hit position is computed using the ray Parameters t_hit, ensuring that reflected
segments contain meaningful geometry rather than single-point artifacts.
Reflections are computed using wall normals derived from the wall_map.
Parameters:
- abs_position (tuple of float):
Starting position (x, y) of the beam in absolute pixel space. - img (numpy.ndarray):
2D array representing the scene. Pixel values listed inwall_valuesare treated as solid walls. - direction_in_degree (float):
Initial direction of the beam in degrees (0° = right, 90° = down). - wall_map (numpy.ndarray):
A map storing wall orientations in degrees for each pixel marked as a wall. These angles define the wall normals used for reflection. - wall_values (list, tuple, set, float, optional):
Pixel values identifying walls. Any pixel in this list causes a collision. If None, pixel value 0.0 is treated as a wall. - img_border_also_collide (bool, optional):
If True, the image borders behave like reflective walls. If False, the ray terminates when leaving the image. Default: False. - reflexion_order (int, optional):
Maximum number of reflections. The ray can rebound this many times before the function terminates. Default: 3. - should_scale (bool, optional):
If True, all emitted points (x, y) are normalized to [0, 1] range. Otherwise absolute pixel positions are returned. Default: True. - should_return_iterative (bool, optional):
Whether to return a RayIterator for step-by-step analysis (default: False). - remove_iterative (bool, optional):
Ignored in this numba version.
Returns:
- list: Nested list structure representing the traced ray and its reflections. Format: ray[beam][point] = (x, y)
2346@numba.njit(cache=True, fastmath=True) 2347def get_all_pixel_coordinates_in_between_numba(x1, y1, x2, y2): 2348 """ 2349 Get all pixel coordinates along a line between two points using Bresenham's algorithm. 2350 2351 https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 2352 2353 Parameters: 2354 - x1 (int): <br> 2355 Starting x-coordinate. 2356 - y1 (int): <br> 2357 Starting y-coordinate. 2358 - x2 (int): <br> 2359 Ending x-coordinate. 2360 - y2 (int): <br> 2361 Ending y-coordinate. 2362 2363 Returns: 2364 - list: 2365 List of (x, y) tuples representing all pixels between the start and end points 2366 """ 2367 coordinates = List.empty_list(TYPE_POINT_INT) 2368 2369 dx = abs(x2 - x1) 2370 dy = abs(y2 - y1) 2371 x, y = x1, y1 2372 2373 sx = 1 if x1 < x2 else -1 2374 sy = 1 if y1 < y2 else -1 2375 2376 if dx > dy: 2377 err = dx / 2.0 2378 while x != x2: 2379 coordinates.append((x, y)) 2380 err -= dy 2381 if err < 0: 2382 y += sy 2383 err += dx 2384 x += sx 2385 else: 2386 err = dy / 2.0 2387 while y != y2: 2388 coordinates.append((x, y)) 2389 err -= dx 2390 if err < 0: 2391 x += sx 2392 err += dy 2393 y += sy 2394 2395 coordinates.append((x2, y2)) # include the last point 2396 return list(coordinates)
Get all pixel coordinates along a line between two points using Bresenham's algorithm.
https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
Parameters:
- x1 (int):
Starting x-coordinate. - y1 (int):
Starting y-coordinate. - x2 (int):
Ending x-coordinate. - y2 (int):
Ending y-coordinate.
Returns:
- list: List of (x, y) tuples representing all pixels between the start and end points