Making Subnautica Scarier with Real-Life Effects

Making Subnautica Scarier with Real-Life Effects

By monitoring the health, oxygen, and depth in Subnautica, let's make the game scarier and more immersive with real life effects!

 

Things used in this project

Hardware components

ESP32S
Espressif ESP32S 
×1  NeoPixel Ring: WS2812 5050 RGB LED
Adafruit NeoPixel Ring: WS2812 5050 RGB LED 
×1  

Software apps and online services

Arduino IDE
Arduino IDE 
    
visual studio codeFor python code
    

 

 

 

Inspiration

Subnautica is a blast, but it was around the time that I started scanning and grappling onto the murderous leviathans like Indiana Jones that I realized I was just too desensitized to fully appreciate the thalassophobia feelings the game is supposed to invoke. It is classified as a horror game, after all! With Subnautica 2 set to release in the coming year, this needed rectified immediately. What to do...

 

 

 

What We're Building

The goal is to increase the immersion and the fear factors. That means we need to have a way of monitoring certain aspects of the game so that we can trigger fun stuff.

To do so we're going to put together a python script that monitors certain aspects of the game. For this, we're monitoring the health, oxygen levels, and depth. Depth will just be to increase immersion. I tend to avoid spoilers so I can fully enjoy the given game, but I did look into what the maximum depth vehicles can go to for exactly this purpose and set the deepest depths I expect to be red. On the way down, we'll flow from one color to the next such that we can visualize how deep we are with color in the room!

Next up is oxygen. If we're going to induce panic, there's nothing quite like the Sonic Music that plays when you're near drowning from <redacted> years ago. It gets us right in the childhood and gets the adrenaline pumping like nothing else. If the oxygen percentage drops below 15% we play the music. If the oxygen levels start going up, we stop the music.

Last is health. Subnautica has a nice display when you get hit, mixing a red backdrop with a broken screen, but adding a real life element really makes it hit home. As we'll get to momentarily, we're using an ESP32 for the led's and the buzzers. When we get hit, we pass a message to the ESP32 and make it buzz us so we feel it in real life.

 

 

 

 

 

Codey Time

The majority of the code is in a python script that monitors the display with pytesseract. I also included a calibration program, which was super nice. Last time I dabbled in this type of thing, it took me way too long to hone in on the part of the screen I wanted to read data from. This time, the calibration program makes it a quick visual process. Much less painful.

 

 

The full python program for adding immersion does a lot. For oxygen and health I ended up doing different processing methods to visually interpret the data. It was a pain but eventually the result was pretty good. The depth is just text and that worked pretty well very quickly.

 

For the 3 effects described, the only one that plays on the computer-run side of things is the sonic underwater music. When we drop below 15% oxygen, it plays until we start increasing in oxygen or, um - the audio finishes playing. For this I moved to pygame and it resolved some really annoying issues I had with other approaches.

The code is attached so it should just work in full, but here are the installs you'll need for it:

pip install opencv-python pyautogui numpy pygame pytesseract

 

 

 

Physical Additions

The last part of the process is the ESP32 side of things. The python program still handles the bulk of the processing, but we continuously listen for commands on the arduino side as well. Depth is passed ongoingly, anytime we get good values (which should be the vast majority of the time). A "buzz" is only passed when we take damage, and the arduino orchestrates playing a buzz through the coin buzzers.

 

To wire to the ESP32 is pretty straightforward. Here is a simple pinout:

D5 --> LED data

VIN --> LED power

GND --> LED GND

D14 --> buzzer red

D2 --> other buzzer's red

GND --> both buzzer's blue

 

 

 

End Result

Now we have a fully functional way to give us anxiety problems when we ignore our oxygen levels for too long, cool lighting to represent the depths we reach, and a way to buzz ourselves when we get too close to a bitey-fish. Now that the code works, it does add a lot of fun to the game and sets the stage for when Subnautica 2 arrives. I look forward to hear any ideas and iterations other people have and make! Hope you enjoyed and have a good one.

 

 

CODE
#include <WiFi.h>
#include <WiFiUdp.h>
#include <FastLED.h>

// Wi-Fi credentials
const char* WIFI_SSID     = "your wifi here";
const char* WIFI_PASSWORD = "your wifi pw here";

const int UDP_PORT = 12345;
WiFiUDP udp;

// for your buzzers
const int MOTOR_PIN1 = 14; 
const int MOTOR_PIN2 = 2;

#define LED_PIN 5
#define NUM_LEDS 120
CRGB leds[NUM_LEDS];

// Short motor activation
unsigned long BUZZ_TIME_MS = 60;
bool buzzing = false;
unsigned long buzzStartTime = 0;

// We define multiple color stops for different depths
// Each entry is { depth in meters, CRGB color }.
struct DepthColor {
  int depth;
  CRGB color;
};

DepthColor depthStops[] = {
  {   0, CRGB::Green   }, // 0 m
  { 300, CRGB::Teal    }, // 300 m
  { 700, CRGB::Blue    }, // 700 m
  {1200, CRGB::Purple  }, // 1200 m
  {1700, CRGB::Red     }  // 1700 m
};
const int NUM_STOPS = sizeof(depthStops)/sizeof(depthStops[0]);

void setup() {
  Serial.begin(115200);
  Serial.println("Subnautica Depth + Multi-Color Gradient + Buzz (FastLED).");

  pinMode(MOTOR_PIN1, OUTPUT);
  pinMode(MOTOR_PIN2, OUTPUT);
  digitalWrite(MOTOR_PIN1, LOW);
  digitalWrite(MOTOR_PIN2, LOW);

  // Setup FastLED
  FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(255);
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();

  // Connect Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWi-Fi connected.");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());

  // UDP
  udp.begin(UDP_PORT);
  Serial.print("Listening on port ");
  Serial.println(UDP_PORT);
}

// Helper to blend between two CRGB colors
CRGB blendCRGB(const CRGB &c1, const CRGB &c2, float fraction) {
  // clamp fraction to [0..1]
  if (fraction < 0) fraction = 0;
  if (fraction > 1) fraction = 1;

  uint8_t r = c1.r + (uint8_t)((c2.r - c1.r) * fraction);
  uint8_t g = c1.g + (uint8_t)((c2.g - c1.g) * fraction);
  uint8_t b = c1.b + (uint8_t)((c2.b - c1.b) * fraction);
  return CRGB(r, g, b);
}

// We find which two stops the depth is between, then interpolate
CRGB getColorForDepth(int depthVal) {
  // If below first stop, clamp to first color
  if (depthVal <= depthStops[0].depth) {
    return depthStops[0].color;
  }
  // If above last stop, clamp to last color
  if (depthVal >= depthStops[NUM_STOPS - 1].depth) {
    return depthStops[NUM_STOPS - 1].color;
  }

  // Otherwise, find the segment of the two stops we fall between
  for (int i = 0; i < NUM_STOPS - 1; i++) {
    int d1 = depthStops[i].depth;
    int d2 = depthStops[i+1].depth;
    if (depthVal >= d1 && depthVal <= d2) {
      // fraction from d1..d2
      float fraction = (float)(depthVal - d1) / (float)(d2 - d1);
      // blend between color[i]..color[i+1]
      return blendCRGB(depthStops[i].color, depthStops[i+1].color, fraction);
    }
  }
  // Fallback (shouldn’t happen)
  return depthStops[NUM_STOPS - 1].color;
}

// Set entire strip to the color for a given depth
void setStripColorForDepth(int depthVal) {
  // get interpolated color
  CRGB c = getColorForDepth(depthVal);
  // Serial.print("Color for depth: ");
  // Serial.println(c);
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = c;
  }
  FastLED.show();
}

void loop() {
  // Check incoming UDP
  int packetSize = udp.parsePacket();
  if (packetSize > 0) {
    static char incomingPacket[128]; 
    int len = udp.read(incomingPacket, 127);
    if (len > 0) {
      incomingPacket[len] = 0;
    }
    String data = String(incomingPacket);
    data.trim();

    Serial.print("Received: ");
    Serial.println(data);

    if (data.startsWith("BUZZ")) {
      Serial.println("Buzz => motors on");
      buzzing = true;
      buzzStartTime = millis();
      digitalWrite(MOTOR_PIN1, HIGH);
      digitalWrite(MOTOR_PIN2, HIGH);
    } 
    else if (data.startsWith("DEPTH:")) {
      String valStr = data.substring(6);
      valStr.trim();
      int depthVal = valStr.toInt();
      Serial.print("Depth: ");
      Serial.println(depthVal);
      setStripColorForDepth(depthVal);
    }
  }

  // turn off motors if time’s up
  if (buzzing) {
    if (millis() - buzzStartTime >= BUZZ_TIME_MS) {
      buzzing = false;
      digitalWrite(MOTOR_PIN1, LOW);
      digitalWrite(MOTOR_PIN2, LOW);
      Serial.println("Motors off");
    }
  }

  delay(5);
}
CODE
import cv2
import pyautogui
import numpy as np

SCALE = 0.1

MAX_RANGE_XY = 10800 
MAX_RANGE_WH = 10800 

# Initial bounding boxes, in actual pixels. Adjust if you already know approximate coords.
depth_vals = [600, 50, 100, 40]     # (x, y, width, height)
health_vals = [50, 850, 60, 60]
oxygen_vals = [200, 850, 60, 60]
temperature_vals = [350, 850, 60, 60]

# Window where we show the main screenshot preview
MAIN_WINDOW = "Calibration Preview"
cv2.namedWindow(MAIN_WINDOW, cv2.WINDOW_NORMAL)

# Window for trackbar controls
TRACKBAR_WINDOW = "Adjust Regions"
cv2.namedWindow(TRACKBAR_WINDOW, cv2.WINDOW_NORMAL)

def val_to_trackbar(pixel_value):
    """Convert an actual pixel value to trackbar units."""
    return int(pixel_value / SCALE)

def trackbar_to_val(trackbar_value):
    """Convert from trackbar units back to float pixel values."""
    return trackbar_value * SCALE

# trackbars for Depth region
cv2.createTrackbar("Depth X", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Depth Y", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Depth W", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Depth H", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Health region
cv2.createTrackbar("Health X", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Health Y", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Health W", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Health H", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Oxygen region
cv2.createTrackbar("O2 X", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("O2 Y", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("O2 W", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("O2 H", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Temperature region
cv2.createTrackbar("Temp X", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Temp Y", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Temp W", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Temp H", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[3]), MAX_RANGE_WH, lambda x: None)

# Windows to display each roi
cv2.namedWindow("Depth ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Health ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Oxygen ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Temperature ROI", cv2.WINDOW_NORMAL)

def safe_crop(img, x, y, w, h):
    """Crop (x, y, w, h) safely from an image, avoiding out-of-bounds errors."""
    h_img, w_img = img.shape[:2]
    x, y, w, h = map(int, [x, y, w, h])
    if x < 0: x = 0
    if y < 0: y = 0
    if x + w > w_img: w = max(0, w_img - x)
    if y + h > h_img: h = max(0, h_img - y)
    if w <= 0 or h <= 0:
        return None
    return img[y:y+h, x:x+w]

while True:
    # Capture current screen
    screenshot = pyautogui.screenshot()
    full_frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)

    # Read trackbar positions, convert to float pixel values
    dx = trackbar_to_val(cv2.getTrackbarPos("Depth X", TRACKBAR_WINDOW))
    dy = trackbar_to_val(cv2.getTrackbarPos("Depth Y", TRACKBAR_WINDOW))
    dw = trackbar_to_val(cv2.getTrackbarPos("Depth W", TRACKBAR_WINDOW))
    dh = trackbar_to_val(cv2.getTrackbarPos("Depth H", TRACKBAR_WINDOW))

    hx = trackbar_to_val(cv2.getTrackbarPos("Health X", TRACKBAR_WINDOW))
    hy = trackbar_to_val(cv2.getTrackbarPos("Health Y", TRACKBAR_WINDOW))
    hw = trackbar_to_val(cv2.getTrackbarPos("Health W", TRACKBAR_WINDOW))
    hh = trackbar_to_val(cv2.getTrackbarPos("Health H", TRACKBAR_WINDOW))

    ox = trackbar_to_val(cv2.getTrackbarPos("O2 X", TRACKBAR_WINDOW))
    oy = trackbar_to_val(cv2.getTrackbarPos("O2 Y", TRACKBAR_WINDOW))
    ow = trackbar_to_val(cv2.getTrackbarPos("O2 W", TRACKBAR_WINDOW))
    oh = trackbar_to_val(cv2.getTrackbarPos("O2 H", TRACKBAR_WINDOW))

    tx = trackbar_to_val(cv2.getTrackbarPos("Temp X", TRACKBAR_WINDOW))
    ty = trackbar_to_val(cv2.getTrackbarPos("Temp Y", TRACKBAR_WINDOW))
    tw = trackbar_to_val(cv2.getTrackbarPos("Temp W", TRACKBAR_WINDOW))
    th = trackbar_to_val(cv2.getTrackbarPos("Temp H", TRACKBAR_WINDOW))

    # Draw rectangles on the main frame to visualize each region
    cv2.rectangle(full_frame, (int(dx), int(dy)), (int(dx + dw), int(dy + dh)), (255, 0, 0), 2)
    cv2.rectangle(full_frame, (int(hx), int(hy)), (int(hx + hw), int(hy + hh)), (0, 255, 0), 2)
    cv2.rectangle(full_frame, (int(ox), int(oy)), (int(ox + ow), int(oy + oh)), (0, 0, 255), 2)
    cv2.rectangle(full_frame, (int(tx), int(ty)), (int(tx + tw), int(ty + th)), (0, 255, 255), 2)

    # Crop each ROI and show in separate windows
    depth_roi = safe_crop(full_frame, dx, dy, dw, dh)
    health_roi = safe_crop(full_frame, hx, hy, hw, hh)
    oxygen_roi = safe_crop(full_frame, ox, oy, ow, oh)
    temp_roi   = safe_crop(full_frame, tx, ty, tw, th)

    cv2.imshow("Depth ROI", depth_roi if depth_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Health ROI", health_roi if health_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Oxygen ROI", oxygen_roi if oxygen_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Temperature ROI", temp_roi if temp_roi is not None else np.zeros((10,10,3), dtype=np.uint8))

    # Show the main preview
    cv2.imshow(MAIN_WINDOW, full_frame)

    # Press 'q' to quit
    if cv2.waitKey(10) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()
CODE
import cv2
import pyautogui
import numpy as np

SCALE = 0.1

MAX_RANGE_XY = 10800 
MAX_RANGE_WH = 10800 

# Initial bounding boxes, in actual pixels. Adjust if you already know approximate coords.
depth_vals = [600, 50, 100, 40]     # (x, y, width, height)
health_vals = [50, 850, 60, 60]
oxygen_vals = [200, 850, 60, 60]
temperature_vals = [350, 850, 60, 60]

# Window where we show the main screenshot preview
MAIN_WINDOW = "Calibration Preview"
cv2.namedWindow(MAIN_WINDOW, cv2.WINDOW_NORMAL)

# Window for trackbar controls
TRACKBAR_WINDOW = "Adjust Regions"
cv2.namedWindow(TRACKBAR_WINDOW, cv2.WINDOW_NORMAL)

def val_to_trackbar(pixel_value):
    """Convert an actual pixel value to trackbar units."""
    return int(pixel_value / SCALE)

def trackbar_to_val(trackbar_value):
    """Convert from trackbar units back to float pixel values."""
    return trackbar_value * SCALE

# trackbars for Depth region
cv2.createTrackbar("Depth X", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Depth Y", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Depth W", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Depth H", TRACKBAR_WINDOW,
                   val_to_trackbar(depth_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Health region
cv2.createTrackbar("Health X", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Health Y", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Health W", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Health H", TRACKBAR_WINDOW,
                   val_to_trackbar(health_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Oxygen region
cv2.createTrackbar("O2 X", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("O2 Y", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("O2 W", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("O2 H", TRACKBAR_WINDOW,
                   val_to_trackbar(oxygen_vals[3]), MAX_RANGE_WH, lambda x: None)

# trackbars for Temperature region
cv2.createTrackbar("Temp X", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[0]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Temp Y", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[1]), MAX_RANGE_XY, lambda x: None)
cv2.createTrackbar("Temp W", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[2]), MAX_RANGE_WH, lambda x: None)
cv2.createTrackbar("Temp H", TRACKBAR_WINDOW,
                   val_to_trackbar(temperature_vals[3]), MAX_RANGE_WH, lambda x: None)

# Windows to display each roi
cv2.namedWindow("Depth ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Health ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Oxygen ROI", cv2.WINDOW_NORMAL)
cv2.namedWindow("Temperature ROI", cv2.WINDOW_NORMAL)

def safe_crop(img, x, y, w, h):
    """Crop (x, y, w, h) safely from an image, avoiding out-of-bounds errors."""
    h_img, w_img = img.shape[:2]
    x, y, w, h = map(int, [x, y, w, h])
    if x < 0: x = 0
    if y < 0: y = 0
    if x + w > w_img: w = max(0, w_img - x)
    if y + h > h_img: h = max(0, h_img - y)
    if w <= 0 or h <= 0:
        return None
    return img[y:y+h, x:x+w]

while True:
    # Capture current screen
    screenshot = pyautogui.screenshot()
    full_frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_RGB2BGR)

    # Read trackbar positions, convert to float pixel values
    dx = trackbar_to_val(cv2.getTrackbarPos("Depth X", TRACKBAR_WINDOW))
    dy = trackbar_to_val(cv2.getTrackbarPos("Depth Y", TRACKBAR_WINDOW))
    dw = trackbar_to_val(cv2.getTrackbarPos("Depth W", TRACKBAR_WINDOW))
    dh = trackbar_to_val(cv2.getTrackbarPos("Depth H", TRACKBAR_WINDOW))

    hx = trackbar_to_val(cv2.getTrackbarPos("Health X", TRACKBAR_WINDOW))
    hy = trackbar_to_val(cv2.getTrackbarPos("Health Y", TRACKBAR_WINDOW))
    hw = trackbar_to_val(cv2.getTrackbarPos("Health W", TRACKBAR_WINDOW))
    hh = trackbar_to_val(cv2.getTrackbarPos("Health H", TRACKBAR_WINDOW))

    ox = trackbar_to_val(cv2.getTrackbarPos("O2 X", TRACKBAR_WINDOW))
    oy = trackbar_to_val(cv2.getTrackbarPos("O2 Y", TRACKBAR_WINDOW))
    ow = trackbar_to_val(cv2.getTrackbarPos("O2 W", TRACKBAR_WINDOW))
    oh = trackbar_to_val(cv2.getTrackbarPos("O2 H", TRACKBAR_WINDOW))

    tx = trackbar_to_val(cv2.getTrackbarPos("Temp X", TRACKBAR_WINDOW))
    ty = trackbar_to_val(cv2.getTrackbarPos("Temp Y", TRACKBAR_WINDOW))
    tw = trackbar_to_val(cv2.getTrackbarPos("Temp W", TRACKBAR_WINDOW))
    th = trackbar_to_val(cv2.getTrackbarPos("Temp H", TRACKBAR_WINDOW))

    # Draw rectangles on the main frame to visualize each region
    cv2.rectangle(full_frame, (int(dx), int(dy)), (int(dx + dw), int(dy + dh)), (255, 0, 0), 2)
    cv2.rectangle(full_frame, (int(hx), int(hy)), (int(hx + hw), int(hy + hh)), (0, 255, 0), 2)
    cv2.rectangle(full_frame, (int(ox), int(oy)), (int(ox + ow), int(oy + oh)), (0, 0, 255), 2)
    cv2.rectangle(full_frame, (int(tx), int(ty)), (int(tx + tw), int(ty + th)), (0, 255, 255), 2)

    # Crop each ROI and show in separate windows
    depth_roi = safe_crop(full_frame, dx, dy, dw, dh)
    health_roi = safe_crop(full_frame, hx, hy, hw, hh)
    oxygen_roi = safe_crop(full_frame, ox, oy, ow, oh)
    temp_roi   = safe_crop(full_frame, tx, ty, tw, th)

    cv2.imshow("Depth ROI", depth_roi if depth_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Health ROI", health_roi if health_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Oxygen ROI", oxygen_roi if oxygen_roi is not None else np.zeros((10,10,3), dtype=np.uint8))
    cv2.imshow("Temperature ROI", temp_roi if temp_roi is not None else np.zeros((10,10,3), dtype=np.uint8))

    # Show the main preview
    cv2.imshow(MAIN_WINDOW, full_frame)

    # Press 'q' to quit
    if cv2.waitKey(10) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()
License
All Rights
Reserved
licensBg
0