446 lines
17 KiB
Rust
446 lines
17 KiB
Rust
use std::io;
|
|
use std::path::PathBuf;
|
|
use chrono::Utc;
|
|
use opencv::{core, imgcodecs, imgproc, highgui, prelude::*};
|
|
|
|
// Simplified watermark overlay module for demo
|
|
mod overlay {
|
|
use anyhow::Result;
|
|
use chrono::{DateTime, Utc};
|
|
use opencv::{core, imgproc, prelude::*};
|
|
use serde::{Deserialize, Serialize};
|
|
use freetype::{Library, Face};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum WatermarkPosition {
|
|
TopLeft,
|
|
TopRight,
|
|
BottomLeft,
|
|
BottomRight,
|
|
Custom(u32, u32),
|
|
}
|
|
|
|
impl Default for WatermarkPosition {
|
|
fn default() -> Self {
|
|
Self::BottomLeft
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WatermarkOptions {
|
|
pub enabled: bool,
|
|
pub position: WatermarkPosition,
|
|
pub font_scale: f64,
|
|
pub thickness: i32,
|
|
pub color: (u8, u8, u8, u8),
|
|
pub background: bool,
|
|
pub background_color: (u8, u8, u8, u8),
|
|
pub padding: i32,
|
|
}
|
|
|
|
impl Default for WatermarkOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
position: WatermarkPosition::BottomLeft,
|
|
font_scale: 0.6,
|
|
thickness: 1,
|
|
color: (255, 255, 255, 255), // White
|
|
background: true,
|
|
background_color: (0, 0, 0, 128), // Semi-transparent black
|
|
padding: 8,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct WatermarkDemo {
|
|
options: WatermarkOptions,
|
|
gps_position: (f64, f64, f64), // lat, lon, alt
|
|
temperature: f32,
|
|
humidity: f32,
|
|
freetype_lib: Library,
|
|
font_face: Face, // Use a static lifetime for simplicity in demo
|
|
}
|
|
|
|
impl WatermarkDemo {
|
|
pub fn new(options: WatermarkOptions) -> Result<Self> {
|
|
let freetype_lib = Library::init()?;
|
|
// Dynamically set font_path based on OS using conditional compilation
|
|
let font_path = if cfg!(target_os = "windows") {
|
|
"C:\\Windows\\Fonts\\arial.ttf" // Common Windows font
|
|
} else if cfg!(target_os = "linux") {
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" // Common Linux font
|
|
} else if cfg!(target_os = "macos") {
|
|
"/System/Library/Fonts/Supplemental/Arial.ttf" // Common macOS font
|
|
} else {
|
|
// Fallback or error for other OS
|
|
eprintln!("Warning: Could not determine a suitable font path for the current OS. Using a default that might not exist.");
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" // Default fallback
|
|
};
|
|
let font_face = freetype_lib.new_face(font_path, 0)?;
|
|
|
|
Ok(Self {
|
|
options,
|
|
gps_position: (34.0522, -118.2437, 85.0), // Default to Los Angeles
|
|
temperature: 25.0,
|
|
humidity: 60.0,
|
|
freetype_lib,
|
|
font_face,
|
|
})
|
|
}
|
|
|
|
pub fn set_gps(&mut self, lat: f64, lon: f64, alt: f64) {
|
|
self.gps_position = (lat, lon, alt);
|
|
}
|
|
|
|
pub fn set_weather(&mut self, temp: f32, humidity: f32) {
|
|
self.temperature = temp;
|
|
self.humidity = humidity;
|
|
}
|
|
|
|
pub fn apply(&mut self, frame: &mut core::Mat, timestamp: DateTime<Utc>) -> Result<()> {
|
|
if !self.options.enabled {
|
|
return Ok(());
|
|
}
|
|
|
|
// Build text lines
|
|
let mut lines = Vec::new();
|
|
|
|
// Timestamp
|
|
lines.push(timestamp.format("%Y-%m-%d %H:%M:%S%.3f").to_string());
|
|
|
|
// GPS coordinates
|
|
let (lat, lon, alt) = self.gps_position;
|
|
lines.push(format!("Lat: {:.6}° Lon: {:.6}° Alt: {:.1}m", lat, lon, alt));
|
|
|
|
// Environment data
|
|
lines.push(format!(
|
|
"Temp: {:.1}°C Humidity: {:.1}%",
|
|
self.temperature, self.humidity
|
|
));
|
|
|
|
// Skip if no content
|
|
if lines.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Get frame dimensions
|
|
let width = frame.cols();
|
|
let height = frame.rows();
|
|
|
|
// Set font size based on options.font_scale (adjust as needed)
|
|
let font_size = (self.options.font_scale * 30.0) as isize; // Example scaling
|
|
self.font_face.set_pixel_sizes(0, font_size as u32)?;
|
|
|
|
let padding = self.options.padding;
|
|
let line_spacing = 4; // Space between lines
|
|
|
|
// Calculate text size and total block size using FreeType
|
|
let mut line_heights = Vec::with_capacity(lines.len());
|
|
let mut max_width = 0;
|
|
let mut total_height = 0;
|
|
|
|
for line in &lines {
|
|
let mut line_width = 0;
|
|
let mut line_height = 0;
|
|
for char in line.chars() {
|
|
self.font_face.load_char(char as usize, freetype::face::LoadFlag::RENDER)?;
|
|
let glyph = self.font_face.glyph();
|
|
let metrics = glyph.metrics();
|
|
line_width += (metrics.horiAdvance / 64) as i32;
|
|
line_height = line_height.max((metrics.height / 64) as i32);
|
|
}
|
|
line_heights.push(line_height);
|
|
max_width = max_width.max(line_width);
|
|
total_height += line_height;
|
|
}
|
|
|
|
let text_block_height = total_height + line_spacing * (lines.len() as i32 - 1) + padding * 2;
|
|
let text_block_width = max_width + padding * 2;
|
|
|
|
// Calculate watermark position
|
|
let (x, y) = match self.options.position {
|
|
WatermarkPosition::TopLeft => (padding, padding),
|
|
WatermarkPosition::TopRight => (width - text_block_width - padding, padding),
|
|
WatermarkPosition::BottomLeft => (padding, height - text_block_height - padding),
|
|
WatermarkPosition::BottomRight => (
|
|
width - text_block_width - padding,
|
|
height - text_block_height - padding
|
|
),
|
|
WatermarkPosition::Custom(x, y) => (x as i32, y as i32),
|
|
};
|
|
|
|
// Draw background rectangle if enabled
|
|
if self.options.background {
|
|
let bg_color = core::Scalar::new(
|
|
self.options.background_color.0 as f64,
|
|
self.options.background_color.1 as f64,
|
|
self.options.background_color.2 as f64,
|
|
self.options.background_color.3 as f64,
|
|
);
|
|
|
|
let rect = core::Rect::new(
|
|
x,
|
|
y,
|
|
text_block_width,
|
|
text_block_height
|
|
);
|
|
|
|
imgproc::rectangle(
|
|
frame,
|
|
rect,
|
|
bg_color,
|
|
-1, // Fill
|
|
imgproc::LINE_8,
|
|
0,
|
|
)?;
|
|
}
|
|
|
|
// Draw text lines using FreeType
|
|
let text_color = core::Scalar::new(
|
|
self.options.color.0 as f64,
|
|
self.options.color.1 as f64,
|
|
self.options.color.2 as f64,
|
|
self.options.color.3 as f64,
|
|
);
|
|
|
|
let mut current_y = y + padding;
|
|
|
|
for (i, line) in lines.iter().enumerate() {
|
|
let mut current_x = x + padding;
|
|
for char in line.chars() {
|
|
self.font_face.load_char(char as usize, freetype::face::LoadFlag::RENDER)?;
|
|
let glyph = self.font_face.glyph();
|
|
let bitmap = glyph.bitmap();
|
|
let metrics = glyph.metrics();
|
|
|
|
let bitmap_width = bitmap.width() as usize;
|
|
let bitmap_rows = bitmap.rows() as usize;
|
|
let bitmap_buffer = bitmap.buffer();
|
|
|
|
// Calculate text position for this character
|
|
let char_x = current_x + (metrics.horiBearingX / 64) as i32;
|
|
let char_y = current_y + (line_heights[i] as f64 * 0.8) as i32 - (metrics.horiBearingY / 64) as i32; // Adjust baseline
|
|
|
|
// Draw the glyph bitmap onto the frame
|
|
for row in 0..bitmap_rows {
|
|
for col in 0..bitmap_width {
|
|
let bitmap_pixel = bitmap_buffer[row * bitmap.pitch() as usize + col];
|
|
if bitmap_pixel > 0 {
|
|
let frame_x = char_x + col as i32;
|
|
let frame_y = char_y + row as i32;
|
|
|
|
// Ensure pixel is within frame bounds
|
|
if frame_x >= 0 && frame_x < width && frame_y >= 0 && frame_y < height {
|
|
let mut pixel = frame.at_2d_mut::<core::Vec3b>(frame_y, frame_x)?;
|
|
pixel[0] = text_color[0] as u8;
|
|
pixel[1] = text_color[1] as u8;
|
|
pixel[2] = text_color[2] as u8;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
current_x += (metrics.horiAdvance / 64) as i32;
|
|
}
|
|
|
|
current_y += line_heights[i] + line_spacing;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Simplified camera module for demo
|
|
mod camera {
|
|
use opencv::{core, prelude::*, videoio};
|
|
use chrono::{DateTime, Utc};
|
|
|
|
pub struct Frame {
|
|
pub image: core::Mat,
|
|
pub timestamp: DateTime<Utc>,
|
|
pub index: u64,
|
|
}
|
|
|
|
impl Frame {
|
|
pub fn new(image: core::Mat, timestamp: DateTime<Utc>, index: u64) -> Self {
|
|
Self { image, timestamp, index }
|
|
}
|
|
}
|
|
|
|
pub struct CameraDemo {
|
|
capture: videoio::VideoCapture,
|
|
frame_count: u64,
|
|
}
|
|
|
|
impl CameraDemo {
|
|
pub fn new(device_id: i32) -> Result<Self, Box<dyn std::error::Error>> {
|
|
let capture = videoio::VideoCapture::new(device_id, videoio::CAP_ANY)?;
|
|
if !capture.is_opened()? {
|
|
return Err(format!("Failed to open camera device {}", device_id).into());
|
|
}
|
|
|
|
Ok(Self {
|
|
capture,
|
|
frame_count: 0,
|
|
})
|
|
}
|
|
|
|
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
|
let capture = videoio::VideoCapture::from_file(path, videoio::CAP_ANY)?;
|
|
if !capture.is_opened()? {
|
|
return Err(format!("Failed to open video file {}", path).into());
|
|
}
|
|
|
|
Ok(Self {
|
|
capture,
|
|
frame_count: 0,
|
|
})
|
|
}
|
|
|
|
pub fn capture_frame(&mut self) -> Result<Frame, Box<dyn std::error::Error>> {
|
|
let mut frame = core::Mat::default();
|
|
if self.capture.read(&mut frame)? {
|
|
if frame.empty() {
|
|
return Err("Captured frame is empty".into());
|
|
}
|
|
|
|
let timestamp = Utc::now();
|
|
let index = self.frame_count;
|
|
self.frame_count += 1;
|
|
|
|
Ok(Frame::new(frame, timestamp, index))
|
|
} else {
|
|
Err("Failed to capture frame".into())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("*** Watermark Overlay Demo ***");
|
|
println!("This demo adds a watermark overlay to frames from a camera or video file");
|
|
println!("Press 'q' to quit");
|
|
println!("Press 'p' to change watermark position");
|
|
println!("Press 'c' to change GPS coordinates");
|
|
println!("Press 't' to change temperature and humidity");
|
|
println!();
|
|
|
|
println!("Choose input source:");
|
|
println!("1. Camera device");
|
|
println!("2. Video file");
|
|
|
|
let mut choice = String::new();
|
|
io::stdin().read_line(&mut choice)?;
|
|
|
|
let mut camera_demo = match choice.trim() {
|
|
"1" => {
|
|
println!("Enter camera device ID (default is 0):");
|
|
let mut device_id = String::new();
|
|
io::stdin().read_line(&mut device_id)?;
|
|
|
|
let device_id = device_id.trim().parse::<i32>().unwrap_or(0);
|
|
println!("Opening camera device {}", device_id);
|
|
camera::CameraDemo::new(device_id)?
|
|
},
|
|
"2" => {
|
|
println!("Enter video file path:");
|
|
let mut file_path = String::new();
|
|
io::stdin().read_line(&mut file_path)?;
|
|
|
|
let file_path = file_path.trim();
|
|
println!("Opening video file: {}", file_path);
|
|
camera::CameraDemo::from_file(file_path)?
|
|
},
|
|
_ => {
|
|
println!("Invalid choice, using default camera (device 0)");
|
|
camera::CameraDemo::new(0)?
|
|
}
|
|
};
|
|
|
|
// Create watermark demo
|
|
let mut watermark_options = overlay::WatermarkOptions::default();
|
|
let mut watermark_demo = overlay::WatermarkDemo::new(watermark_options.clone())?;
|
|
|
|
highgui::named_window("Watermark Overlay Demo", highgui::WINDOW_NORMAL)?;
|
|
|
|
loop {
|
|
match camera_demo.capture_frame() {
|
|
Ok(frame) => {
|
|
// Apply watermark to the frame
|
|
let mut display_frame = frame.image.clone();
|
|
if let Err(e) = watermark_demo.apply(&mut display_frame, frame.timestamp) {
|
|
println!("Error applying watermark: {}", e);
|
|
}
|
|
|
|
// Display the frame
|
|
highgui::imshow("Watermark Overlay Demo", &display_frame)?;
|
|
|
|
// Handle key presses
|
|
let key = highgui::wait_key(30)?;
|
|
|
|
match key as u8 as char {
|
|
'q' => break, // Quit
|
|
'p' => {
|
|
// Cycle through positions
|
|
watermark_options.position = match watermark_options.position {
|
|
overlay::WatermarkPosition::TopLeft => overlay::WatermarkPosition::TopRight,
|
|
overlay::WatermarkPosition::TopRight => overlay::WatermarkPosition::BottomRight,
|
|
overlay::WatermarkPosition::BottomRight => overlay::WatermarkPosition::BottomLeft,
|
|
overlay::WatermarkPosition::BottomLeft => overlay::WatermarkPosition::TopLeft,
|
|
_ => overlay::WatermarkPosition::TopLeft,
|
|
};
|
|
watermark_demo = overlay::WatermarkDemo::new(watermark_options.clone())?;
|
|
println!("Changed watermark position: {:?}", watermark_options.position);
|
|
},
|
|
'c' => {
|
|
// Change to some predefined GPS locations
|
|
static LOCATIONS: [(f64, f64, f64, &str); 4] = [
|
|
(34.0522, -118.2437, 85.0, "Los Angeles"),
|
|
(40.7128, -74.0060, 10.0, "New York"),
|
|
(51.5074, -0.1278, 20.0, "London"),
|
|
(35.6762, 139.6503, 40.0, "Tokyo"),
|
|
];
|
|
|
|
static mut LOCATION_INDEX: usize = 0;
|
|
|
|
unsafe {
|
|
LOCATION_INDEX = (LOCATION_INDEX + 1) % LOCATIONS.len();
|
|
let (lat, lon, alt, name) = LOCATIONS[LOCATION_INDEX];
|
|
watermark_demo.set_gps(lat, lon, alt);
|
|
println!("Changed location to {}: Lat={}, Lon={}, Alt={}m", name, lat, lon, alt);
|
|
}
|
|
},
|
|
't' => {
|
|
// Cycle through different weather conditions
|
|
static CONDITIONS: [(f32, f32, &str); 4] = [
|
|
(25.0, 60.0, "Warm and Humid"),
|
|
(32.0, 80.0, "Hot and Very Humid"),
|
|
(15.0, 40.0, "Cool and Dry"),
|
|
(5.0, 30.0, "Cold and Dry"),
|
|
];
|
|
|
|
static mut CONDITION_INDEX: usize = 0;
|
|
|
|
unsafe {
|
|
CONDITION_INDEX = (CONDITION_INDEX + 1) % CONDITIONS.len();
|
|
let (temp, humidity, desc) = CONDITIONS[CONDITION_INDEX];
|
|
watermark_demo.set_weather(temp, humidity);
|
|
println!("Changed weather to {}: Temp={} deg C, Humidity={}%", desc, temp, humidity);
|
|
}
|
|
},
|
|
_ => {}
|
|
}
|
|
},
|
|
Err(e) => {
|
|
println!("Error capturing frame: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
highgui::destroy_all_windows()?;
|
|
|
|
Ok(())
|
|
}
|