import cv2 import numpy as np import time import json import os from collections import deque from datetime import datetime from flask import Flask, Response, render_template import threading import logging import csv logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('/home/al/python/pump_capture.log'), logging.StreamHandler() ] ) app = Flask(__name__) class CameraWatchdog: def __init__(self, timeout=10): self.timeout = timeout self.last_frame_time = time.time() self.running = True def update(self): self.last_frame_time = time.time() def watch(self, camera, restart_callback): while self.running: if time.time() - self.last_frame_time > self.timeout: logging.warning("Camera feed stalled - attempting restart") restart_callback() time.sleep(1) class PumpCapture: def __init__(self, fps=10): # Default ROI positions (will be adjusted if calibration exists) self.roi1_default = (249, 244, 6, 6) self.roi2_default = (426, 220, 6, 6) self.roi3_default = (231, 220, 6, 6) # ROI size (width, height) - stays constant self.roi_size = (6, 6) # Calculate relative offsets from ROI1 to ROI2 and ROI3 # These offsets are from ROI1's top-left corner self.roi2_offset = ( self.roi2_default[0] - self.roi1_default[0], # x offset: +177 self.roi2_default[1] - self.roi1_default[1] # y offset: -24 ) self.roi3_offset = ( self.roi3_default[0] - self.roi1_default[0], # x offset: -18 self.roi3_default[1] - self.roi1_default[1] # y offset: -24 ) # Load calibrated positions or use defaults self.roi1, self.roi2, self.roi3 = self.load_roi_positions() logging.info(f"ROI1: {self.roi1}") logging.info(f"ROI2: {self.roi2}") logging.info(f"ROI3: {self.roi3}") self.setup_camera() self.watchdog = CameraWatchdog() self.watchdog_thread = threading.Thread(target=self.watchdog.watch, args=(self.camera, self.restart_camera)) self.watchdog_thread.daemon = True self.watchdog_thread.start() self.fps = fps self.frame_interval = 1.0 / fps self.resolution = (640, 480) self.current_frame = None self.roi1_brightness = 0 self.roi2_brightness = 0 self.roi3_brightness = 0 # Store brightness history for short-term rolling mean (frame-based) self.brightness_history = deque(maxlen=50) # Store logged 2-second brightness values for longer term means self.short_mean_history = deque(maxlen=3) # Last 3 readings (6 seconds) self.long_mean_history = deque(maxlen=60) # Last 60 readings (2 minutes) self.lock = threading.Lock() # Store last brightness value for derivative calculation self.last_roi1_brightness = None # Averages CSV setup - now v4 self.averages_csv = '/home/al/python/brightness_averages_v4.csv' if not os.path.exists(self.averages_csv): with open(self.averages_csv, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['Date', 'Time', 'ROI1_Brightness', 'ROI1_Derivative', 'ROI1_Rolling_Mean', 'ROI1_Short_Mean', 'ROI1_Long_Mean', 'ROI2_Brightness', 'ROI3_Brightness']) # Start a thread to check for calibration updates self.calibration_check_thread = threading.Thread(target=self.check_calibration_updates) self.calibration_check_thread.daemon = True self.calibration_check_thread.start() def load_roi_positions(self): """ Load ROI positions from calibration file if available, otherwise use defaults """ calibration_file = '/home/al/python/roi_calibration.json' if not os.path.exists(calibration_file): logging.info("No calibration file found, using default ROI positions") return self.roi1_default, self.roi2_default, self.roi3_default try: with open(calibration_file, 'r') as f: data = json.load(f) # Check if calibration is recent (less than 7 days old) calibration_age = time.time() - data.get('timestamp', 0) if calibration_age > 7 * 24 * 3600: # 7 days logging.warning(f"Calibration file is {calibration_age/86400:.1f} days old, using defaults") return self.roi1_default, self.roi2_default, self.roi3_default # Get the new ROI1 center new_center = data.get('roi1_center') if not new_center or len(new_center) != 2: logging.warning("Invalid calibration data, using defaults") return self.roi1_default, self.roi2_default, self.roi3_default # Calculate new ROI1 top-left corner from center # Center is at (x+3, y+3) for a 6x6 ROI, so top-left is (center_x-3, center_y-3) roi1_x = new_center[0] - 3 roi1_y = new_center[1] - 3 roi1 = (roi1_x, roi1_y, 6, 6) # Calculate ROI2 and ROI3 based on relative offsets roi2 = ( roi1_x + self.roi2_offset[0], roi1_y + self.roi2_offset[1], 6, 6 ) roi3 = ( roi1_x + self.roi3_offset[0], roi1_y + self.roi3_offset[1], 6, 6 ) logging.info(f"Loaded calibrated ROI positions from {data.get('datetime', 'unknown time')}") logging.info(f"ROI1 center moved from {self.get_roi_center(self.roi1_default)} to {new_center}") return roi1, roi2, roi3 except Exception as e: logging.error(f"Error loading calibration file: {e}, using defaults") return self.roi1_default, self.roi2_default, self.roi3_default def get_roi_center(self, roi): """Calculate the center point of an ROI""" return (roi[0] + roi[2]//2, roi[1] + roi[3]//2) def check_calibration_updates(self): """ Periodically check for calibration updates and reload ROIs if needed Checks once per hour """ calibration_file = '/home/al/python/roi_calibration.json' last_modified = 0 while True: try: time.sleep(3600) # Check every hour if os.path.exists(calibration_file): current_modified = os.path.getmtime(calibration_file) if current_modified > last_modified: logging.info("Calibration file updated, reloading ROI positions...") # Reload ROI positions new_roi1, new_roi2, new_roi3 = self.load_roi_positions() # Update with thread safety with self.lock: self.roi1 = new_roi1 self.roi2 = new_roi2 self.roi3 = new_roi3 logging.info("ROI positions updated successfully") last_modified = current_modified except Exception as e: logging.error(f"Error checking calibration updates: {e}") def setup_camera(self): self.camera = cv2.VideoCapture(0) if not self.camera.isOpened(): raise RuntimeError("Failed to open camera") self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640) self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) self.camera.set(cv2.CAP_PROP_FPS, 10) def restart_camera(self): logging.info("Restarting camera...") with self.lock: self.camera.release() time.sleep(1) self.setup_camera() def log_average(self): now = datetime.now() roi1_current = self.roi1_brightness # Calculate derivative roi1_derivative = 0.0 if self.last_roi1_brightness is not None: roi1_derivative = roi1_current - self.last_roi1_brightness # Calculate rolling mean (simple average of frame brightness history) if len(self.brightness_history) > 0: roi1_rolling_mean = np.mean(list(self.brightness_history)) else: roi1_rolling_mean = roi1_current # Add current brightness to the logged means history self.short_mean_history.append(roi1_current) self.long_mean_history.append(roi1_current) # Calculate short mean (last 3 logged readings ≈ 6 seconds) if len(self.short_mean_history) > 0: roi1_short_mean = np.mean(list(self.short_mean_history)) else: roi1_short_mean = roi1_current # Calculate long mean (last 60 logged readings ≈ 2 minutes) if len(self.long_mean_history) > 0: roi1_long_mean = np.mean(list(self.long_mean_history)) else: roi1_long_mean = roi1_current # Update last brightness value for next derivative calculation self.last_roi1_brightness = roi1_current # Write to CSV with open(self.averages_csv, 'a', newline='') as f: writer = csv.writer(f) writer.writerow([ now.strftime('%Y-%m-%d'), now.strftime('%H:%M:%S'), f"{roi1_current:.1f}", f"{roi1_derivative:.1f}", f"{roi1_rolling_mean:.1f}", f"{roi1_short_mean:.1f}", f"{roi1_long_mean:.1f}", f"{self.roi2_brightness:.1f}", f"{self.roi3_brightness:.1f}" ]) def get_roi_brightness(self, frame, roi): x, y, w, h = roi # Ensure ROI is within frame bounds if x < 0 or y < 0 or x+w > frame.shape[1] or y+h > frame.shape[0]: logging.warning(f"ROI out of bounds: {roi}") return 0 roi_area = frame[y:y+h, x:x+w] return np.mean(roi_area) def generate_frames(self): while True: with self.lock: if self.current_frame is not None: frame = self.current_frame.copy() info_lines = [ f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", f"ROI1 Brightness: {self.roi1_brightness:.1f}", f"ROI2 Brightness: {self.roi2_brightness:.1f}", f"ROI3 Brightness: {self.roi3_brightness:.1f}" ] for i, line in enumerate(info_lines): y_pos = 30 + (i * 30) cv2.putText(frame, line, (11, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 4) cv2.putText(frame, line, (10, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) # Draw ROI boxes for i, roi in enumerate([self.roi1, self.roi2, self.roi3], 1): x, y, w, h = roi color = (0, 255, 0) if i == 1 else (255, 0, 0) cv2.rectangle(frame, (x, y), (x+w, y+h), color, 1) cv2.putText(frame, f"ROI{i}", (x+w+5, y+h//2), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) ret, buffer = cv2.imencode('.jpg', frame) if ret: frame_bytes = buffer.tobytes() yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n') time.sleep(0.1) def capture(self): logging.info("Starting brightness capture with auto-calibration...") last_log_update = time.time() while True: try: frame_start = time.time() ret, frame = self.camera.read() if not ret: logging.error("Cannot read from camera") time.sleep(1) continue self.watchdog.update() with self.lock: self.current_frame = frame.copy() self.roi1_brightness = self.get_roi_brightness(frame, self.roi1) self.roi2_brightness = self.get_roi_brightness(frame, self.roi2) self.roi3_brightness = self.get_roi_brightness(frame, self.roi3) self.brightness_history.append(self.roi1_brightness) # Log to CSV every 2 seconds if time.time() - last_log_update >= 2: self.log_average() last_log_update = time.time() elapsed = time.time() - frame_start sleep_time = max(0, self.frame_interval - elapsed) time.sleep(sleep_time) except Exception as e: logging.error(f"Error in capture loop: {str(e)}") time.sleep(1) def __del__(self): self.watchdog.running = False if hasattr(self, 'camera'): self.camera.release() capture = PumpCapture() @app.route('/') def index(): return render_template('index.html') @app.route('/video_feed') def video_feed(): return Response(capture.generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame') if __name__ == "__main__": capture_thread = threading.Thread(target=capture.capture) capture_thread.daemon = True capture_thread.start() app.run(host='0.0.0.0', port=5000, threaded=True)