"""Python supporting code for the GTA model."""
from dataclasses import dataclass
import math
import random
import time
import numpy
import scipy.spatial
try:
import PIL
except ModuleNotFoundError as e:
raise RuntimeError(
"GTA scenarios require the PIL module;" ' try "pip install pillow"'
) from e
try:
import cv2
except ModuleNotFoundError as e:
raise RuntimeError(
"GTA scenarios require the cv2 module;" ' try "pip install opencv-python"'
) from e
from scenic.core.distributions import (
Distribution,
Normal,
Options,
Range,
Samplable,
distributionMethod,
toDistribution,
)
from scenic.core.geometry import *
from scenic.core.lazy_eval import valueInContext
from scenic.core.regions import GridRegion, PointSetRegion
import scenic.core.utils as utils
from scenic.core.vectors import VectorField
from scenic.core.workspaces import Workspace
import scenic.simulators.gta.center_detection as center_detection
import scenic.simulators.gta.img_modf as img_modf
import scenic.simulators.gta.messages as messages
from scenic.simulators.utils.colors import Color
from scenic.syntax.veneer import verbosePrint
### Abstract GTA interface
class GTA:
@staticmethod
def Config(scene):
ego = scene.egoObject
vehicles = [GTA.Vehicle(car) for car in scene.objects if car is not ego]
cameraLoc = GTA.langToGTACoords(ego.position)
cameraHeading = GTA.langToGTAHeading(ego.heading)
params = dict(scene.params)
time = int(round(params.pop("time"))) % 1440
hour, minute = divmod(time, 60)
assert hour < 24, scene.params["time"]
weather = params.pop("weather")
for param in params:
print(f'WARNING: unused scene parameter "{param}"')
return messages.Formal_Config(
cameraLoc, [hour, minute], weather, vehicles, cameraHeading
)
@staticmethod
def Vehicle(car):
loc3 = GTA.langToGTACoords(car.position)
heading = GTA.langToGTAHeading(car.heading)
scol = list(Color.realToByte(car.color))
return messages.Vehicle(car.model.name, scol, loc3, heading)
@staticmethod
def langToGTACoords(point):
x, y, _ = point
return [x, y, 60]
@staticmethod
def langToGTAHeading(heading):
h = math.degrees(heading)
return (h + 360) % 360
### Map
[docs]class Map:
"""Represents roads and obstacles in GTA, extracted from a map image.
This code handles images from the `GTA V Interactive Map <https://gta-5-map.com/>`_,
rendered with the "Road" setting.
Args:
imagePath (str): path to image file
Ax (float): width of one pixel in GTA coordinates
Ay (float): height of one pixel in GTA coordinates
Bx (float): GTA X-coordinate of bottom-left corner of image
By (float): GTA Y-coordinate of bottom-left corner of image
"""
def __init__(self, imagePath, Ax, Ay, Bx, By):
self.Ax, self.Ay = Ax, Ay
self.Bx, self.By = Bx, By
if imagePath != None:
startTime = time.time()
# open image
image = PIL.Image.open(imagePath)
self.sizeX, self.sizeY = image.size
# create version of image for display
de = img_modf.get_edges(image).convert("RGB")
self.displayImage = cv2.cvtColor(numpy.array(de), cv2.COLOR_RGB2BGR)
# detect edges of roads
ed = center_detection.compute_midpoints(img_data=image, kernelsize=5)
edges, tangents = [], []
for (y, x), tangent in ed.items():
edges.append(self.gridToScenicCoords((x, y)))
tangents.append(tangent)
self.edges = numpy.array(edges)
self.edgeTangents = numpy.array(tangents)
# build k-D tree
self.edgeTree = scipy.spatial.KDTree(self.edges)
# identify points on roads
self.roadArray = numpy.array(
img_modf.convert_black_white(img_data=image).convert("L"), dtype=int
)
totalTime = time.time() - startTime
verbosePrint(f"Created GTA map from image in {totalTime:.2f} seconds.")
@staticmethod
def fromFile(path):
startTime = time.time()
with numpy.load(path) as data:
Ax, Ay, Bx, By, sizeX, sizeY = data["misc"]
m = Map(None, Ax, Ay, Bx, By)
m.sizeX, m.sizeY = sizeX, sizeY
m.displayImage = data["displayImage"]
m.edges = data["edges"]
m.edgeTangents = data["tangents"]
m.edgeTree = scipy.spatial.KDTree(m.edges) # rebuild k-D tree
m.roadArray = data["roadArray"]
totalTime = time.time() - startTime
verbosePrint(f"Loaded GTA map in {totalTime:.2f} seconds.")
return m
def dumpToFile(self, path):
misc = numpy.array((self.Ax, self.Ay, self.Bx, self.By, self.sizeX, self.sizeY))
numpy.savez_compressed(
path,
misc=misc,
displayImage=self.displayImage,
edges=self.edges,
tangents=self.edgeTangents,
roadArray=self.roadArray,
)
@property
@utils.cached
def roadDirection(self):
return VectorField("roadDirection", self.roadHeadingAt)
@property
@utils.cached
def roadRegion(self):
return GridRegion(
"road",
self.roadArray,
self.Ax,
self.Ay,
self.Bx,
self.By,
orientation=self.roadDirection,
)
@property
@utils.cached
def curbRegion(self):
return PointSetRegion("curb", self.edges, orientation=self.roadDirection)
def gridToScenicCoords(self, point):
x, y = point[0], point[1]
return ((self.Ax * x) + self.Bx, (self.Ay * y) + self.By)
def gridToScenicHeading(self, heading):
return heading - (math.pi / 2)
def scenicToGridCoords(self, point):
x, y = point[0], point[1]
return ((x - self.Bx) / self.Ax, (y - self.By) / self.Ay)
def scenicToGridHeading(self, heading):
return heading + (math.pi / 2)
@distributionMethod
def roadHeadingAt(self, point):
# find closest edge
point2d = (point[0], point[1])
distance, location = self.edgeTree.query(point2d)
# get direction of edge
return self.gridToScenicHeading(self.edgeTangents[location])
def show(self, plt):
plt.imshow(self.displayImage)
[docs]class MapWorkspace(Workspace):
"""Workspace whose rendering is handled by a Map"""
def __init__(self, mappy, region):
super().__init__(region)
self.map = mappy
def scenicToSchematicCoords(self, coords):
return self.map.scenicToGridCoords(coords)
def show2D(self, plt):
return self.map.show(plt)
@property
def minimumZoomSize(self):
return 40 / min(abs(self.map.Ax), abs(self.map.Ay))
### Car models and colors
[docs]@dataclass(frozen=True)
class CarModel:
"""A model of car in GTA.
Attributes:
name (str): name of model in GTA
width (float): width of this model of car
length (float): length of this model of car
viewAngle (float): view angle in radians (default is 90 degrees)
Class Attributes:
models: dict mapping model names to the corresponding `CarModel`
"""
name: str
width: float
length: float
viewAngle: float = math.radians(90)
@classmethod
def uniformModel(self):
return Options(self.modelProbs.keys())
@classmethod
def egoModel(self):
return self.models["BLISTA"]
@classmethod
def defaultModel(self):
return Options(self.modelProbs)
def __str__(self):
return f"<CarModel {self.name}>"
CarModel.modelProbs = {
CarModel("BLISTA", 1.75871, 4.10139): 1,
CarModel("BUS", 2.9007, 13.202): 0,
CarModel("NINEF", 2.07699, 4.50658): 1,
CarModel("ASEA", 1.83066, 4.45861): 1,
CarModel("BALLER", 2.10791, 5.10333): 1,
CarModel("BISON", 2.29372, 5.4827): 1,
CarModel("BUFFALO", 2.04265, 5.07782): 1,
CarModel("BOBCATXL", 2.37944, 5.78222): 1,
CarModel("DOMINATOR", 1.9353, 4.9355): 1,
CarModel("GRANGER", 3.02698, 5.94577): 1,
CarModel("JACKAL", 2.00041, 4.91436): 1,
CarModel("ORACLE", 2.07787, 5.12544): 1,
CarModel("PATRIOT", 2.26679, 5.13695): 1,
CarModel("PRANGER", 3.02698, 5.94577): 1,
}
CarModel.models = {model.name: model for model in CarModel.modelProbs}
CarColor = Color # for backwards compatibility