-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathras.py
More file actions
executable file
·228 lines (190 loc) · 7.62 KB
/
ras.py
File metadata and controls
executable file
·228 lines (190 loc) · 7.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
#
# Copyright (C) 2023 Quico Augustijn
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
#
# Route advisor steering for Euro Truck Simulator 2
#
# Monitor the red line of the in-game route advisor and steer with the mouse
# controls to keep the truck on road.
import time
import numpy as np
from mss import mss
from pynput import mouse
# Region of interest (roi) of the screen
#
# Preferably, half the 'width' parameter and the 'left' parameter added together
# should be the center position of the blue triangle on the route advisor and
# this should also be the center of your region of interest.
# A higher 'width' will give a greater field-of-view, but will be less
# performant.
# The defaults were determined when the game was in fullscreen.
roi_top = 850
roi_left = 1625
roi_width = 128
roi_height = 1
# Maximum amount of allowed colored pixels
#
# Whenever the amount of detected colored pixels exceeds this value, declare the
# route guidance as unreliable and stop taking action.
error_max = roi_width * 0.75
# Horizontal position of the center of the road (blue triangle on the route
# advisor)
#
# This should be the center of the region of interest. If you have changed the
# region of interest parameters so that this is not the case, set the position
# here manually.
center_static = roi_width / 2 - 0.5
#center_static = 63.5
# PID parameters:
# To calculate how much steering is needed, a PID controller is used.
# Kp (Proportional control): Constant that specifies how much the controller
# should respond to the error value.
# Ki (Integral control): Constant that specifies how much the controller should
# respond to the error value related to past iterations.
# Kd (Derivative control): Constant that specifies how much the controller
# should respond to changes in error value.
Kp = float(1 / 4)
Ki = float(0)
Kd = float(1 / 5)
# Iteration speed:
# How fast the software should iterate and adjust the steering controls. A
# higher number means shorter iteration sleep time.
iteration_speed = 75
# Color range
#
# Color values used to determine whether a pixel is colored or not. You can
# adjust these colors to your liking, but the default reddish color should be
# fine. Colors range from 0 to 255
red_min = 200
red_max = 255
green_min = 0
green_max = 35
blue_min = 0
blue_max = 35
# Maximum amount of characters to print on one line
print_max_length = 64
# Character to print at the end of a printed line
print_end_char = '\r'
# Print text that overwrites the last printed line
def print_line(string):
rest = print_max_length - len(string)
print(string[:print_max_length], ' ' * rest, end=print_end_char)
# Clamp a value between a given limit
def clamp(value, limit):
if value > limit:
return limit
elif value < -limit:
return -limit
else:
return value
# Function that decides if a pixel is colored
def is_pixel_colored(red, green, blue):
return red >= red_min and red <= red_max and \
green >= green_min and green <= green_max and \
blue >= blue_min and blue <= blue_max
# Grab an instance of mss (for taking screenshots)
sct = mss()
# Grab an instance of the mouse controls
mouse = mouse.Controller()
# Create region of interest dictionary
roi = {"top": roi_top, "left": roi_left, "width": roi_width, "height": roi_height}
# Initialize error variables
error = None
old_error = None
# Interval (sleep) time to use
interval_time = (1 / iteration_speed)
# Variables for the PID controller
proportional = integral = derivative = 0
# Run for as long as we're allowed to live
while(True):
# Grab the region of interest of the screen
screen = sct.grab(roi)
img = np.array(screen)
# The image should be only one row in height
row = img[0]
# Get the length (amount of pixels horizontally)
length = len(img[0])
# Set the initial pixel information
first_pixel = length # First colored pixel
last_pixel = 0 # Last colored pixel
found_first = False # If the first colored pixel is found
found_last = False # If the last colored pixel is found
# Iterate over each pixel in the row
for x in range(length):
# Current pixel
pixel = row[x];
# Get each RGB value of this pixel
red = pixel[2]
green = pixel[1]
blue = pixel[0]
# Only save this position if it is the very first colored pixel
if x < first_pixel and is_pixel_colored(red, green, blue):
first_pixel = x
found_first = True
# Only save this position if it is the very last colored pixel
if x > last_pixel and is_pixel_colored(red, green, blue):
last_pixel = x
found_last = True
# Only respond when colored pixels were found
if not found_first or not found_last:
print_line("Route out of sight")
proportional = integral = derivative = 0
else:
# Calculate the width of the colored area
width = last_pixel - first_pixel
# Calculate the center of the colored area
center_position = first_pixel + float(width) / 2
if width < error_max:
# Calculate the error value
error = center_position - center_static
else:
# Do not use the error value
print_line("Route detection unreliable")
error = None
# Do not respond on first iteration (change in error is not known yet)
if error != None and old_error != None:
# Calculate the difference in error compared to the previous iteration
change = error - old_error
# Proportional control
proportional = error
# Integral control
integral = integral + error * interval_time
integral = clamp(integral, roi_width / 2)
# Derivative control
derivative = change / interval_time
# Calculate the output
output = Kp * proportional + Ki * integral + Kd * derivative
output = clamp(output, roi_width / 2)
# Now move the mouse
mouse.move(output, 0) # change in y is 0
# Print status
txt = "P {:.2f}".format(proportional) + " " \
"I {:.2f}".format(integral) + " " \
"D {:.2f}".format(derivative) + " " \
"Offset {:.2f}".format(error) + " " \
"Change {:.2f}".format(change)
print_line(txt)
# Record the current error for the next iteration
old_error = error
# Give our actions a little time to take effect
time.sleep(interval_time)