"""Python supporting code for the GTA model."""
import math
import time
import colorsys
from collections import namedtuple
import numpy
import scipy.spatial
import PIL
import cv2
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.core.distributions import (Samplable, Distribution, Range, Normal, Options,
distributionMethod, toDistribution)
from scenic.core.lazy_eval import valueInContext
from scenic.core.workspaces import Workspace
from scenic.core.vectors import VectorField
from scenic.core.regions import PointSetRegion, GridRegion
from scenic.core.object_types import Mutator
import scenic.core.utils as utils
from scenic.core.geometry import *
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')))
minute = time % 60
hour = int((time - minute) / 60)
assert hour < 24
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(CarColor.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)
self.edgeData = {
self.gridToScenicCoords((x, y)): datum
for (y, x), datum in ed.items()
}
self.orderedCurbPoints = list(self.edgeData.keys())
# build k-D tree
self.edgeTree = scipy.spatial.cKDTree(self.orderedCurbPoints)
# 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, allow_pickle=True) 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.edgeData = { tuple(e): center_detection.EdgeData(*rest) for e, *rest in data['edges'] }
m.orderedCurbPoints = list(m.edgeData.keys())
m.edgeTree = scipy.spatial.cKDTree(m.orderedCurbPoints) # 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))
edges = numpy.array([(edge,) + tuple(datum) for edge, datum in self.edgeData.items()])
roadArray = self.roadArray
numpy.savez_compressed(path,
misc=misc, displayImage=self.displayImage,
edges=edges, 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.orderedCurbPoints,
kdTree=self.edgeTree,
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
distance, location = self.edgeTree.query(point)
closest = tuple(self.edgeTree.data[location])
# get direction of edge
return self.gridToScenicHeading(self.edgeData[closest].tangent)
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 show(self, plt):
plt.gca().set_aspect('equal')
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]class CarModel:
"""A model of car in GTA.
Attributes:
name (str): name of model in GTA
width (float): width of this model of car
height (float): height 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`
"""
def __init__(self, name, width, height, viewAngle=math.radians(90)):
super(CarModel, self).__init__()
self.name = name
self.width = width
self.height = height
self.viewAngle = viewAngle
@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 }
[docs]class CarColor(namedtuple('CarColor', ['r', 'g', 'b'])):
"""A car color as an RGB tuple."""
@classmethod
def withBytes(cls, color):
return cls._make(c / 255.0 for c in color)
@staticmethod
def realToByte(color):
return tuple(int(round(255 * c)) for c in color)
[docs] @staticmethod
def defaultColor():
"""Default color distribution for cars.
The distribution starts with a base distribution over 9 discrete colors,
then adds Gaussian HSL noise. The base distribution uses color popularity
statistics from a `2012 DuPont survey`_.
.. _2012 DuPont survey: https://web.archive.org/web/20121229065631/http://www2.dupont.com/Media_Center/en_US/color_popularity/Images_2012/DuPont2012ColorPopularity.pdf
"""
baseColors = {
(248, 248, 248): 0.24, # white
(50, 50, 50): 0.19, # black
(188, 185, 183): 0.16, # silver
(130, 130, 130): 0.15, # gray
(194, 92, 85): 0.10, # red
(75, 119, 157): 0.07, # blue
(197, 166, 134): 0.05, # brown/beige
(219, 191, 105): 0.02, # yellow/gold
(68, 160, 135): 0.02, # green
}
converted = { CarColor.withBytes(color): prob for color, prob in baseColors.items() }
baseColor = Options(converted)
# TODO improve this?
hueNoise = Normal(0, 0.1)
satNoise = Normal(0, 0.1)
lightNoise = Normal(0, 0.1)
return NoisyColorDistribution(baseColor, hueNoise, satNoise, lightNoise)
[docs]class NoisyColorDistribution(Distribution):
"""A distribution given by HSL noise around a base color.
Arguments:
baseColor (RGB tuple): base color
hueNoise (float): noise to add to base hue
satNoise (float): noise to add to base saturation
lightNoise (float): noise to add to base lightness
"""
def __init__(self, baseColor, hueNoise, satNoise, lightNoise):
super().__init__(baseColor, hueNoise, satNoise, lightNoise, valueType=CarColor)
self.baseColor = baseColor
self.hueNoise = hueNoise
self.satNoise = satNoise
self.lightNoise = lightNoise
@staticmethod
def addNoiseTo(color, hueNoise, lightNoise, satNoise):
hue, lightness, saturation = colorsys.rgb_to_hls(*color)
hue = max(0, min(1, hue + hueNoise))
lightness = max(0, min(1, lightness + lightNoise))
saturation = max(0, min(1, saturation + satNoise))
return colorsys.hls_to_rgb(hue, lightness, saturation)
def sampleGiven(self, value):
bc = value[self.baseColor]
return CarColor(*self.addNoiseTo(bc, value[self.hueNoise],
value[self.lightNoise], value[self.satNoise]))
def evaluateInner(self, context):
self.baseColor = valueInContext(self.baseColor, context)
self.hueNoise = valueInContext(self.hueNoise, context)
self.satNoise = valueInContext(self.satNoise, context)
self.lightNoise = valueInContext(self.lightNoise, context)
[docs]class CarColorMutator(Mutator):
"""Mutator that adds Gaussian HSL noise to the ``color`` property."""
def appliedTo(self, obj):
hueNoise = random.gauss(0, 0.05)
satNoise = random.gauss(0, 0.05)
lightNoise = random.gauss(0, 0.05)
color = NoisyColorDistribution.addNoiseTo(obj.color, hueNoise, lightNoise, satNoise)
return tuple([obj.copyWith(color=color), True]) # allow further mutation