Skip to content

Line Map

Grayscale track image used by line-following B-bots: dark pixels are the line and the analog sensor reading is proportional to the surface darkness.

LineTrackMap

LineTrackMap(image_path, resolution, colors=None)

Mapa de pista de linha (line following).

Carrega uma imagem em tons de cinza onde a linha preta é a trajetória a ser seguida. Diferente do :class:OccupancyGridMap, os pixels escuros são a pista (livre) e os claros são o fundo (fora da linha). A imagem é usada para amostrar sensores analógicos (ex.: TCRT5000), retornando um valor em [0, 1] proporcional à "escuridão" da superfície (1 = linha preta, 0 = fundo branco).

Source code in src/hivebr/engine/line_map.py
def __init__(self, image_path: str, resolution: float, colors: dict = None):
    self.resolution = float(resolution)
    self.colors = colors if colors is not None else {}

    resolved_path = self._resolve_path(image_path)

    gray = np.array(Image.open(resolved_path).convert("L"), dtype=np.float32)
    self.height, self.width = gray.shape
    self.width_m = self.width * self.resolution
    self.height_m = self.height * self.resolution

    # Inverte verticalmente para alinhar a origem da imagem (topo) com a
    # origem do mundo (canto inferior esquerdo), como no occupancy grid.
    self.darkness = np.flipud(1.0 - gray / 255.0)

    try:
        # A imagem original tem o topo no maior y do mundo (a origem do
        # mundo fica na linha de baixo da imagem), que já coincide com a
        # projeção da câmera (screen_to_world/world_to_screen). Por isso
        # NÃO invertemos aqui — o array `darkness` é que é invertido para a
        # amostragem analógica.
        self.pygame_image = pygame.image.load(resolved_path).convert()
    except Exception as exc:  # pragma: no cover - depends on pygame setup
        print(f"Warning: could not load line map image in Pygame: {exc}")
        self.pygame_image = None

resolution instance-attribute

resolution = float(resolution)

colors instance-attribute

colors = colors if colors is not None else {}

width_m instance-attribute

width_m = self.width * self.resolution

height_m instance-attribute

height_m = self.height * self.resolution

darkness instance-attribute

darkness = np.flipud(1.0 - gray / 255.0)

pygame_image instance-attribute

pygame_image = pygame.image.load(resolved_path).convert()

world_to_pixel

world_to_pixel(x, y)

Converte coordenadas de mundo (m) para pixel (col, row).

Source code in src/hivebr/engine/line_map.py
def world_to_pixel(self, x: float, y: float) -> tuple[int, int]:
    """Converte coordenadas de mundo (m) para pixel ``(col, row)``."""
    col = int(x / self.resolution)
    row = self.height - 1 - int(y / self.resolution)
    return col, row

sample

sample(x, y, aperture=0.0)

Amostra analógica em (x, y).

aperture é o raio (em metros) da área média em torno do ponto, representando o "campo de visão" do sensor. Fora do mapa retorna 0 (fundo claro / fora da linha).

Source code in src/hivebr/engine/line_map.py
def sample(self, x: float, y: float, aperture: float = 0.0) -> float:
    """Amostra analógica em ``(x, y)``.

    ``aperture`` é o raio (em metros) da área média em torno do ponto,
    representando o "campo de visão" do sensor. Fora do mapa retorna ``0``
    (fundo claro / fora da linha).
    """
    ix = int(x / self.resolution)
    iy = int(y / self.resolution)

    if ix < 0 or ix >= self.width or iy < 0 or iy >= self.height:
        return 0.0

    radius = int(round(aperture / self.resolution))
    if radius <= 0:
        return float(self.darkness[iy, ix])

    x0 = max(0, ix - radius)
    x1 = min(self.width, ix + radius + 1)
    y0 = max(0, iy - radius)
    y1 = min(self.height, iy + radius + 1)
    return float(self.darkness[y0:y1, x0:x1].mean())

is_on_line

is_on_line(x, y, threshold=0.5)
Source code in src/hivebr/engine/line_map.py
def is_on_line(self, x: float, y: float, threshold: float = 0.5) -> bool:
    return self.sample(x, y) >= threshold

draw

draw(screen, camera)

Desenha a imagem da pista no mundo (fundo da arena).

Source code in src/hivebr/engine/line_map.py
def draw(self, screen: pygame.Surface, camera):
    """Desenha a imagem da pista no mundo (fundo da arena)."""
    width_px = int(self.width_m * camera.scale)
    height_px = int(self.height_m * camera.scale)
    if width_px <= 0 or height_px <= 0:
        return

    sx, sy = camera.world_to_screen(0.0, self.height_m)
    if self.pygame_image is not None:
        scaled = pygame.transform.scale(self.pygame_image, (width_px, height_px))
        screen.blit(scaled, (sx, sy))

    border = self.colors.get("line_border", [0, 0, 0])
    pygame.draw.rect(screen, tuple(border), (sx, sy, width_px, height_px), 2)

to_gray

to_gray()

Retorna a imagem em tons de cinza (uint8, linha 0 no topo).

Source code in src/hivebr/engine/line_map.py
def to_gray(self) -> np.ndarray:
    """Retorna a imagem em tons de cinza (``uint8``, linha 0 no topo)."""
    return np.flipud((1.0 - self.darkness) * 255.0).astype(np.uint8)

from_gray classmethod

from_gray(gray, resolution, colors=None)

Cria um mapa de linha a partir de um array uint8 (H, W).

A linha 0 do array corresponde ao topo da imagem (maior y do mundo), como em uma imagem lida do disco.

Source code in src/hivebr/engine/line_map.py
@classmethod
def from_gray(cls, gray: np.ndarray, resolution: float, colors: dict = None):
    """Cria um mapa de linha a partir de um array ``uint8`` (H, W).

    A linha 0 do array corresponde ao topo da imagem (maior ``y`` do
    mundo), como em uma imagem lida do disco.
    """
    instance = cls.__new__(cls)
    instance.resolution = float(resolution)
    instance.colors = colors if colors is not None else {}

    gray = np.asarray(gray, dtype=np.uint8)
    if gray.ndim == 3:
        gray = gray[:, :, 0]
    instance.height, instance.width = gray.shape
    instance.width_m = instance.width * instance.resolution
    instance.height_m = instance.height * instance.resolution
    instance.darkness = np.flipud(1.0 - gray.astype(np.float32) / 255.0)

    try:
        rgb = np.ascontiguousarray(np.repeat(gray[:, :, None], 3, axis=2))
        instance.pygame_image = pygame.image.frombuffer(
            rgb.tobytes(), (instance.width, instance.height), "RGB"
        ).copy()
    except Exception:  # pragma: no cover - depends on pygame setup
        instance.pygame_image = None

    return instance