Skip to content

Commit a5d338b

Browse files
author
Dave
committed
Fixed a few issues including switching to auto mode from manual after recording if there is already a detection. Correct default cofiguration. Add a seperate calibration program (still needs more work). Added a hard coded slow move when going back to home position as could be extreme movement depending on the last tracked movement position. Add gitattributes so large doc files/images not included in release.
1 parent 3801d1b commit a5d338b

File tree

5 files changed

+310
-8
lines changed

5 files changed

+310
-8
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
docs/ export-ignore
2+
Pimoroni_pan_tilt_hat/ export-ignore
3+
README.md export-ignore

calibrate_web.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env python3
2+
"""
3+
calibrate_web.py
4+
5+
A web-based calibration tool for adjusting the degree movement per pixel.
6+
It uses Picamera2 to capture images, a Flask web server to serve a web page,
7+
and allows you to click on the image to specify a target point.
8+
The script then calculates the required pan/tilt adjustments (using the same
9+
calculation as in your main program) and moves the camera.
10+
After the move, a new image is shown for comparison.
11+
12+
On startup, the camera is moved to its home position (as defined in your configuration).
13+
"""
14+
15+
import time
16+
import cv2
17+
from flask import Flask, render_template, Response, request, jsonify
18+
from picamera2 import Picamera2
19+
import pan_tilt_control # Your pan/tilt control module
20+
import my_configuration as config # Your configuration file
21+
22+
# Calibration parameters from configuration
23+
PAN_DEG_PER_PIXEL = config.PAN_DEG_PER_PIXEL
24+
TILT_DEG_PER_PIXEL = config.TILT_DEG_PER_PIXEL
25+
PAN_INVERT = config.PAN_INVERT
26+
TILT_INVERT = config.TILT_INVERT
27+
28+
app = Flask(__name__)
29+
30+
# Initialize and configure Picamera2 using the resolution from your config.
31+
# (Your main program uses config.MAIN_STREAM_RESOLUTION for configuration.)
32+
picam2 = Picamera2()
33+
preview_config = picam2.create_preview_configuration(main={"size": config.MAIN_STREAM_RESOLUTION})
34+
picam2.configure(preview_config)
35+
picam2.start()
36+
time.sleep(1) # Allow time for auto exposure/white balance to settle
37+
38+
# Get the actual main stream resolution (this should match what your main program uses)
39+
main_resolution = picam2.stream_configuration("main")["size"]
40+
41+
42+
@app.route("/")
43+
def index():
44+
# Render the calibration page, passing the actual main resolution and a timestamp.
45+
return render_template("index_calibrate.html",
46+
timestamp=time.time(),
47+
main_resolution=main_resolution)
48+
49+
50+
@app.route("/capture_image")
51+
def capture_image():
52+
"""
53+
Capture an image from the camera and return it as a JPEG.
54+
This endpoint is used by the web page to populate the canvases.
55+
"""
56+
try:
57+
img = picam2.capture_array("main")
58+
except Exception as e:
59+
return f"Error capturing image: {e}", 500
60+
61+
ret, jpeg = cv2.imencode('.jpg', img)
62+
if not ret:
63+
return "Failed to encode image", 500
64+
return Response(jpeg.tobytes(), mimetype="image/jpeg")
65+
66+
67+
@app.route("/calibrate", methods=["POST"])
68+
def calibrate():
69+
"""
70+
Receives JSON data with the clicked (x, y) coordinates (in full-resolution pixels),
71+
calculates the offset from the center of the main stream (as computed by the camera),
72+
applies the degrees-per-pixel conversion (with inversion if needed), and commands
73+
the camera to move.
74+
75+
Returns:
76+
- The current angles,
77+
- The pixel offset (x and y),
78+
- The calculated delta (in degrees),
79+
- The new angles,
80+
- And a message.
81+
"""
82+
data = request.get_json()
83+
if not data or "x" not in data or "y" not in data:
84+
return jsonify({"error": "Missing coordinates"}), 400
85+
86+
try:
87+
x = float(data["x"])
88+
y = float(data["y"])
89+
except Exception:
90+
return jsonify({"error": "Invalid coordinates"}), 400
91+
92+
main_w, main_h = main_resolution
93+
center_x = main_w / 2.0
94+
center_y = main_h / 2.0
95+
96+
# Compute the pixel offset from the center (exactly as in your main code)
97+
offset_x = x - center_x
98+
offset_y = y - center_y
99+
100+
# Calculate the required angle adjustments (in degrees)
101+
delta_pan = offset_x * PAN_DEG_PER_PIXEL
102+
delta_tilt = offset_y * TILT_DEG_PER_PIXEL
103+
if PAN_INVERT:
104+
delta_pan = -delta_pan
105+
if TILT_INVERT:
106+
delta_tilt = -delta_tilt
107+
108+
# Get current angles from your pan_tilt_control module
109+
current_pan, current_tilt = pan_tilt_control.get_current_angles()
110+
new_pan = current_pan + delta_pan
111+
new_tilt = current_tilt + delta_tilt
112+
113+
# Command the camera to move to the new angles (using your existing move_to function)
114+
pan_tilt_control.move_to(new_pan, new_tilt)
115+
time.sleep(2) # Allow time for the move to complete
116+
117+
response = {
118+
"current_angles": {"pan": current_pan, "tilt": current_tilt},
119+
"pixel_offset": {"x": offset_x, "y": offset_y},
120+
"delta": {"pan": delta_pan, "tilt": delta_tilt},
121+
"new_angles": {"pan": new_pan, "tilt": new_tilt},
122+
"message": "Camera moved. See the updated view below."
123+
}
124+
return jsonify(response)
125+
126+
127+
if __name__ == "__main__":
128+
# On startup, move the camera to its home position as specified in your config.
129+
# If HOME_PAN or HOME_TILT are not defined, default to 0.
130+
home_pan = getattr(config, "HOME_PAN", 0)
131+
home_tilt = getattr(config, "HOME_TILT", 0)
132+
print(f"Moving camera to home position: pan {home_pan}, tilt {home_tilt}")
133+
pan_tilt_control.move_to(home_pan, home_tilt)
134+
135+
# Run the Flask app with the reloader disabled to prevent double initialization.
136+
app.run(host="0.0.0.0", port=5000, debug=True, use_reloader=False)

main.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ def set_mode():
487487
mode = request.args.get("mode", "auto")
488488
if mode.lower() == "auto":
489489
water_pistol.stop()
490+
target_tracker.reset()
490491
pan_tilt_control.move_to(HOME_PAN, HOME_TILT, steps=MOVE_STEPS, step_delay=MOVE_STEP_DELAY)
491492
auto_mode = True
492493
logger.info("Switched to AUTO mode")
@@ -592,13 +593,16 @@ def __init__(self, move_steps, move_step_delay):
592593
self.move_steps = move_steps
593594
self.move_step_delay = move_step_delay
594595
# Move to home on init
596+
time.sleep(1.0)
595597
self.home()
596598

597599
def home(self):
598600
pan_tilt_control.move_to(
599601
HOME_PAN, HOME_TILT,
600-
steps=self.move_steps,
601-
step_delay=self.move_step_delay
602+
# steps=self.move_steps,
603+
# step_delay=self.move_step_delay
604+
steps=10,
605+
step_delay=0.1
602606
)
603607

604608
def set_target_by_pixels(self, offset_x, offset_y):
@@ -635,8 +639,10 @@ def do_home():
635639
self.is_moving = True
636640
pan_tilt_control.move_to(
637641
HOME_PAN, HOME_TILT,
638-
steps=self.move_steps,
639-
step_delay=self.move_step_delay
642+
# steps=self.move_steps,
643+
# step_delay=self.move_step_delay
644+
steps=10,
645+
step_delay=0.1
640646
)
641647
self.is_moving = False
642648
threading.Thread(target=do_home, daemon=True).start()
@@ -728,6 +734,14 @@ def update_detections(self, has_detections):
728734
def is_target_acquired(self):
729735
return self.target_acquired
730736

737+
def reset(self):
738+
# Force a fresh
739+
self.detection_timestamps.clear()
740+
self.target_acquired = False
741+
self.last_detection_time = None
742+
743+
logger.info("[TargetTracker] State has been reset.")
744+
731745
# Simple detection container
732746
class Detection:
733747
def __init__(self, coords, category, conf, metadata, picam2, imx500):

my_configuration.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
REPLAY_PIN = 14
3030

3131
# How many degrees to move per pixel offset when tracking an object
32-
PAN_DEG_PER_PIXEL = 0.016
33-
TILT_DEG_PER_PIXEL = 0.016
32+
PAN_DEG_PER_PIXEL = 0.044
33+
TILT_DEG_PER_PIXEL = 0.045
3434

3535
# Reverse directions if needed (if you go up instead of down and left instead of right)
3636
PAN_INVERT = True # If True, pan movement is inverted
@@ -52,8 +52,8 @@
5252
HOME_TILT = 20.0
5353

5454
# Move smoothing, adjust to make the movement smoother or more aggressive
55-
MOVE_STEPS = 1
56-
MOVE_STEP_DELAY = 0.00
55+
MOVE_STEPS = 5
56+
MOVE_STEP_DELAY = 0.05
5757

5858
# Fire water pistol on detections
5959
WATER_PISTOL_ARMED = True

templates/index_calibrate.html

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Camera Calibration Tool</title>
6+
<style>
7+
body { font-family: Arial, sans-serif; margin: 20px; }
8+
canvas { border: 1px solid #333; margin-bottom: 20px; }
9+
#beforeContainer, #afterContainer { margin-bottom: 30px; }
10+
#instructions { margin-bottom: 20px; }
11+
</style>
12+
</head>
13+
<body>
14+
<h1>Camera Calibration Tool</h1>
15+
<div id="instructions">
16+
<p><strong>Step 1:</strong> The top canvas shows the current camera image with a green cross marking its center.</p>
17+
<p>Click on the image where you want that center to be relocated (your target point). A second green cross will appear at that point.</p>
18+
</div>
19+
20+
<div id="beforeContainer">
21+
<h2>Before Movement</h2>
22+
<canvas id="beforeCanvas"></canvas>
23+
</div>
24+
25+
<div id="afterContainer" style="display: none;">
26+
<h2>After Movement</h2>
27+
<canvas id="afterCanvas"></canvas>
28+
</div>
29+
30+
<div id="results"></div>
31+
32+
<script>
33+
// Use the main resolution passed from the server.
34+
const imageWidth = {{ main_resolution[0] }};
35+
const imageHeight = {{ main_resolution[1] }};
36+
37+
// Set up canvases.
38+
const beforeCanvas = document.getElementById("beforeCanvas");
39+
const afterCanvas = document.getElementById("afterCanvas");
40+
beforeCanvas.width = imageWidth;
41+
beforeCanvas.height = imageHeight;
42+
afterCanvas.width = imageWidth;
43+
afterCanvas.height = imageHeight;
44+
45+
const beforeCtx = beforeCanvas.getContext("2d");
46+
const afterCtx = afterCanvas.getContext("2d");
47+
48+
// Global variable to store the clicked coordinate (in full resolution).
49+
let clickedCoord = null;
50+
51+
// Load an image from /capture_image.
52+
function loadCapture(callback) {
53+
const img = new Image();
54+
img.onload = function() {
55+
callback(img);
56+
};
57+
// Add a timestamp to bypass caching.
58+
img.src = "/capture_image?ts=" + new Date().getTime();
59+
}
60+
61+
// Draw a green cross at the center of the canvas.
62+
function drawCenterCross(ctx, width, height) {
63+
ctx.strokeStyle = "lime";
64+
ctx.lineWidth = 2;
65+
const centerX = width / 2;
66+
const centerY = height / 2;
67+
const size = 10;
68+
ctx.beginPath();
69+
ctx.moveTo(centerX - size, centerY);
70+
ctx.lineTo(centerX + size, centerY);
71+
ctx.moveTo(centerX, centerY - size);
72+
ctx.lineTo(centerX, centerY + size);
73+
ctx.stroke();
74+
}
75+
76+
// Draw a green cross at a given (x, y) coordinate.
77+
function drawClickCross(ctx, x, y) {
78+
ctx.strokeStyle = "lime";
79+
ctx.lineWidth = 2;
80+
const size = 10;
81+
ctx.beginPath();
82+
ctx.moveTo(x - size, y);
83+
ctx.lineTo(x + size, y);
84+
ctx.moveTo(x, y - size);
85+
ctx.lineTo(x, y + size);
86+
ctx.stroke();
87+
}
88+
89+
// Draw the "before" image with overlays.
90+
function drawBeforeImage() {
91+
loadCapture(function(img) {
92+
beforeCtx.clearRect(0, 0, imageWidth, imageHeight);
93+
beforeCtx.drawImage(img, 0, 0, imageWidth, imageHeight);
94+
drawCenterCross(beforeCtx, imageWidth, imageHeight);
95+
// If a click has been made, mark that point.
96+
if (clickedCoord) {
97+
drawClickCross(beforeCtx, clickedCoord.x, clickedCoord.y);
98+
}
99+
});
100+
}
101+
102+
// Initially load the before image.
103+
drawBeforeImage();
104+
105+
// Handle clicks on the before canvas.
106+
beforeCanvas.addEventListener("click", function(event) {
107+
const rect = beforeCanvas.getBoundingClientRect();
108+
// Convert the click coordinates to full-resolution coordinates.
109+
const clickX = (event.clientX - rect.left) * (imageWidth / beforeCanvas.clientWidth);
110+
const clickY = (event.clientY - rect.top) * (imageHeight / beforeCanvas.clientHeight);
111+
clickedCoord = { x: clickX, y: clickY };
112+
113+
// Redraw the before image with the clicked cross.
114+
drawBeforeImage();
115+
116+
// Send the clicked coordinates to the server for calibration.
117+
fetch("/calibrate", {
118+
method: "POST",
119+
headers: { "Content-Type": "application/json" },
120+
body: JSON.stringify({ x: clickX, y: clickY })
121+
})
122+
.then(response => response.json())
123+
.then(data => {
124+
// Display calibration details.
125+
document.getElementById("results").innerHTML = `
126+
<p><strong>Current Angles:</strong> Pan ${data.current_angles.pan.toFixed(2)}°, Tilt ${data.current_angles.tilt.toFixed(2)}°</p>
127+
<p><strong>Pixel Offset:</strong> X: ${data.pixel_offset.x.toFixed(0)} pixels, Y: ${data.pixel_offset.y.toFixed(0)} pixels</p>
128+
<p><strong>Delta (Angle Adjustment):</strong> Pan ${data.delta.pan.toFixed(2)}°, Tilt ${data.delta.tilt.toFixed(2)}°</p>
129+
<p><strong>New Angles:</strong> Pan ${data.new_angles.pan.toFixed(2)}°, Tilt ${data.new_angles.tilt.toFixed(2)}°</p>
130+
<p>${data.message}</p>
131+
`;
132+
133+
// Show the after canvas and load the new image.
134+
document.getElementById("afterContainer").style.display = "block";
135+
loadCapture(function(imgAfter) {
136+
afterCtx.clearRect(0, 0, imageWidth, imageHeight);
137+
afterCtx.drawImage(imgAfter, 0, 0, imageWidth, imageHeight);
138+
drawCenterCross(afterCtx, imageWidth, imageHeight);
139+
});
140+
})
141+
.catch(err => {
142+
console.error("Error:", err);
143+
document.getElementById("results").innerText = "Error: " + err;
144+
});
145+
});
146+
</script>
147+
</body>
148+
</html>
149+

0 commit comments

Comments
 (0)