Merge branch 'master' into patch-865636

This commit is contained in:
IgorA100 authored and GitHub committed 2026-07-06 18:00:46 +03:00
commit ebf785e2fd
25 files changed
+1406 -97

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
--
-- Add AI Models tables for ZoneMinder
--
-- AI Datasets table - stores datasets like COCO, ImageNet, etc.
CREATE TABLE IF NOT EXISTS `AI_Datasets` (
`Id` int(10) unsigned NOT NULL auto_increment,
`Name` varchar(64) NOT NULL,
`Description` TEXT,
`Version` varchar(32),
`NumClasses` int(10) unsigned NOT NULL,
PRIMARY KEY (`Id`),
UNIQUE KEY `AI_Datasets_Name_idx` (`Name`)
) ENGINE=InnoDB;
-- AI Models table - stores AI model implementations
CREATE TABLE IF NOT EXISTS `AI_Models` (
`Id` int(10) unsigned NOT NULL auto_increment,
`Name` varchar(64) NOT NULL,
`Description` TEXT,
`ModelPath` varchar(255),
`Framework` enum('TensorFlow','PyTorch','ONNX','OpenVINO','TensorRT','Other') NOT NULL default 'ONNX',
`Version` varchar(32),
`DatasetId` int(10) unsigned,
`Enabled` tinyint(1) unsigned NOT NULL default 0,
PRIMARY KEY (`Id`),
UNIQUE KEY `AI_Models_Name_idx` (`Name`),
FOREIGN KEY (`DatasetId`) REFERENCES `AI_Datasets` (`Id`) ON DELETE SET NULL
) ENGINE=InnoDB;
-- AI Object Classes table - stores object classes from datasets
CREATE TABLE IF NOT EXISTS `AI_Object_Classes` (
`Id` int(10) unsigned NOT NULL auto_increment,
`DatasetId` int(10) unsigned NOT NULL,
`ClassName` varchar(64) NOT NULL,
`ClassIndex` int(10) unsigned NOT NULL,
`Description` TEXT,
PRIMARY KEY (`Id`),
KEY `AI_Object_Classes_DatasetId_idx` (`DatasetId`),
UNIQUE KEY `AI_Object_Classes_Dataset_Class_idx` (`DatasetId`, `ClassName`),
FOREIGN KEY (`DatasetId`) REFERENCES `AI_Datasets` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- AI Detection Settings table - stores detection settings per monitor and object class
CREATE TABLE IF NOT EXISTS `AI_Detection_Settings` (
`Id` int(10) unsigned NOT NULL auto_increment,
`MonitorId` int(10) unsigned NULL,
`ObjectClassId` int(10) unsigned NOT NULL,
`Enabled` tinyint(1) unsigned NOT NULL default 1,
`ReportDetection` tinyint(1) unsigned NOT NULL default 1,
`ConfidenceThreshold` tinyint(3) unsigned NOT NULL default 50,
`BoxColor` varchar(7) NOT NULL default '#FF0000',
PRIMARY KEY (`Id`),
KEY `AI_Detection_Settings_MonitorId_idx` (`MonitorId`),
KEY `AI_Detection_Settings_ObjectClassId_idx` (`ObjectClassId`),
UNIQUE KEY `AI_Detection_Settings_Monitor_Object_idx` (`MonitorId`, `ObjectClassId`),
FOREIGN KEY (`MonitorId`) REFERENCES `Monitors` (`Id`) ON DELETE CASCADE,
FOREIGN KEY (`ObjectClassId`) REFERENCES `AI_Object_Classes` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- AI Detections table - stores actual detection results
CREATE TABLE IF NOT EXISTS `AI_Detections` (
`Id` BIGINT unsigned NOT NULL auto_increment,
`EventId` BIGINT unsigned NOT NULL,
`FrameId` BIGINT unsigned,
`ObjectClassId` int(10) unsigned NOT NULL,
`Confidence` decimal(5,4) NOT NULL,
`BoundingBoxX` int(10) unsigned,
`BoundingBoxY` int(10) unsigned,
`BoundingBoxWidth` int(10) unsigned,
`BoundingBoxHeight` int(10) unsigned,
`DetectedAt` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`Id`),
KEY `AI_Detections_EventId_idx` (`EventId`),
KEY `AI_Detections_FrameId_idx` (`FrameId`),
KEY `AI_Detections_ObjectClassId_idx` (`ObjectClassId`),
FOREIGN KEY (`EventId`) REFERENCES `Events` (`Id`) ON DELETE CASCADE,
FOREIGN KEY (`FrameId`) REFERENCES `Frames` (`Id`) ON DELETE SET NULL,
FOREIGN KEY (`ObjectClassId`) REFERENCES `AI_Object_Classes` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB;
+110
View File
@@ -0,0 +1,110 @@
--
-- Pre-populate COCO 2017 Dataset with 80 object classes
--
-- Insert COCO 2017 Dataset
INSERT IGNORE INTO AI_Datasets (Id, Name, Description, Version, NumClasses) VALUES
(1, 'COCO', 'Microsoft Common Objects in Context', '2017', 80);
-- Insert all 80 COCO object classes with correct indices (0-79)
INSERT IGNORE INTO AI_Object_Classes (DatasetId, ClassName, ClassIndex, Description) VALUES
(1, 'person', 0, 'Person'),
(1, 'bicycle', 1, 'Bicycle'),
(1, 'car', 2, 'Car'),
(1, 'motorcycle', 3, 'Motorcycle'),
(1, 'airplane', 4, 'Airplane'),
(1, 'bus', 5, 'Bus'),
(1, 'train', 6, 'Train'),
(1, 'truck', 7, 'Truck'),
(1, 'boat', 8, 'Boat'),
(1, 'traffic light', 9, 'Traffic light'),
(1, 'fire hydrant', 10, 'Fire hydrant'),
(1, 'stop sign', 11, 'Stop sign'),
(1, 'parking meter', 12, 'Parking meter'),
(1, 'bench', 13, 'Bench'),
(1, 'bird', 14, 'Bird'),
(1, 'cat', 15, 'Cat'),
(1, 'dog', 16, 'Dog'),
(1, 'horse', 17, 'Horse'),
(1, 'sheep', 18, 'Sheep'),
(1, 'cow', 19, 'Cow'),
(1, 'elephant', 20, 'Elephant'),
(1, 'bear', 21, 'Bear'),
(1, 'zebra', 22, 'Zebra'),
(1, 'giraffe', 23, 'Giraffe'),
(1, 'backpack', 24, 'Backpack'),
(1, 'umbrella', 25, 'Umbrella'),
(1, 'handbag', 26, 'Handbag'),
(1, 'tie', 27, 'Tie'),
(1, 'suitcase', 28, 'Suitcase'),
(1, 'frisbee', 29, 'Frisbee'),
(1, 'skis', 30, 'Skis'),
(1, 'snowboard', 31, 'Snowboard'),
(1, 'sports ball', 32, 'Sports ball'),
(1, 'kite', 33, 'Kite'),
(1, 'baseball bat', 34, 'Baseball bat'),
(1, 'baseball glove', 35, 'Baseball glove'),
(1, 'skateboard', 36, 'Skateboard'),
(1, 'surfboard', 37, 'Surfboard'),
(1, 'tennis racket', 38, 'Tennis racket'),
(1, 'bottle', 39, 'Bottle'),
(1, 'wine glass', 40, 'Wine glass'),
(1, 'cup', 41, 'Cup'),
(1, 'fork', 42, 'Fork'),
(1, 'knife', 43, 'Knife'),
(1, 'spoon', 44, 'Spoon'),
(1, 'bowl', 45, 'Bowl'),
(1, 'banana', 46, 'Banana'),
(1, 'apple', 47, 'Apple'),
(1, 'sandwich', 48, 'Sandwich'),
(1, 'orange', 49, 'Orange'),
(1, 'broccoli', 50, 'Broccoli'),
(1, 'carrot', 51, 'Carrot'),
(1, 'hot dog', 52, 'Hot dog'),
(1, 'pizza', 53, 'Pizza'),
(1, 'donut', 54, 'Donut'),
(1, 'cake', 55, 'Cake'),
(1, 'chair', 56, 'Chair'),
(1, 'couch', 57, 'Couch'),
(1, 'potted plant', 58, 'Potted plant'),
(1, 'bed', 59, 'Bed'),
(1, 'dining table', 60, 'Dining table'),
(1, 'toilet', 61, 'Toilet'),
(1, 'tv', 62, 'TV'),
(1, 'laptop', 63, 'Laptop'),
(1, 'mouse', 64, 'Mouse'),
(1, 'remote', 65, 'Remote'),
(1, 'keyboard', 66, 'Keyboard'),
(1, 'cell phone', 67, 'Cell phone'),
(1, 'microwave', 68, 'Microwave'),
(1, 'oven', 69, 'Oven'),
(1, 'toaster', 70, 'Toaster'),
(1, 'sink', 71, 'Sink'),
(1, 'refrigerator', 72, 'Refrigerator'),
(1, 'book', 73, 'Book'),
(1, 'clock', 74, 'Clock'),
(1, 'vase', 75, 'Vase'),
(1, 'scissors', 76, 'Scissors'),
(1, 'teddy bear', 77, 'Teddy bear'),
(1, 'hair drier', 78, 'Hair drier'),
(1, 'toothbrush', 79, 'Toothbrush');
-- Create default detection settings for common security monitoring objects
-- Only enable person and vehicles by default
INSERT IGNORE INTO AI_Detection_Settings (MonitorId, ObjectClassId, Enabled, ReportDetection, ConfidenceThreshold, BoxColor)
SELECT NULL, Id, 1, 1,
CASE
WHEN ClassName = 'person' THEN 60
ELSE 50
END,
CASE
WHEN ClassName = 'person' THEN '#FF0000'
WHEN ClassName = 'car' THEN '#0000FF'
WHEN ClassName = 'truck' THEN '#0066FF'
WHEN ClassName = 'bus' THEN '#0099FF'
WHEN ClassName = 'motorcycle' THEN '#00CCFF'
ELSE '#808080'
END
FROM AI_Object_Classes
WHERE DatasetId = (SELECT Id FROM AI_Datasets WHERE Name = 'COCO' LIMIT 1)
AND ClassName IN ('person', 'car', 'truck', 'bus', 'motorcycle');
+9
View File
@@ -610,6 +610,11 @@ CREATE TABLE `Monitors` (
`Analysing` enum('None','Always') NOT NULL default 'Always',
`AnalysisSource` enum('Primary','Secondary') NOT NULL DEFAULT 'Primary',
`AnalysisImage` enum('FullColour','YChannel') NOT NULL DEFAULT 'FullColour',
`AnalysisImageOpacity` TINYINT UNSIGNED NOT NULL DEFAULT '128',
`ObjectDetection` VARCHAR(16) NOT NULL DEFAULT 'none',
`ObjectDetectionModel` VARCHAR(255) NOT NULL DEFAULT '',
`ObjectDetectionObjectThreshold` FLOAT NOT NULL DEFAULT 0.4,
`ObjectDetectionNMSThreshold` FLOAT NOT NULL DEFAULT 0.25,
`Recording` enum('None', 'OnMotion', 'Always') NOT NULL default 'Always',
`Enabled` tinyint(3) unsigned NOT NULL default '1',
`DecodingEnabled` tinyint(3) unsigned NOT NULL default '1',
@@ -1467,6 +1472,10 @@ source @PKGDATADIR@/db/triggers.sql
source @PKGDATADIR@/db/manufacturers.sql
source @PKGDATADIR@/db/models.sql
-- AI object-detection data model: tables then the COCO 2017 seed.
source @PKGDATADIR@/db/AI_Models.sql
source @PKGDATADIR@/db/coco_dataset.sql
--
-- Apply the initial configuration
--
+251
View File
@@ -0,0 +1,251 @@
--
-- AI object-detection data model.
--
-- Adds the per-monitor object-detection columns and the AI_* tables used by
-- the object-detection management UI, then seeds the COCO 2017 dataset.
--
-- All statements are idempotent so re-running (or running after a fresh
-- install that already has these objects) is a no-op.
--
--
-- Monitors columns
--
-- AnalysisImageOpacity: alpha applied when compositing the analysis/annotated
-- image over the source frame (0-255).
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Monitors' AND column_name = 'AnalysisImageOpacity') > 0,
"SELECT 'Column AnalysisImageOpacity already exists in Monitors'",
"ALTER TABLE `Monitors` ADD COLUMN `AnalysisImageOpacity` TINYINT UNSIGNED NOT NULL DEFAULT '128' AFTER `AnalysisImage`"
));
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ObjectDetection: name of the detection backend for this monitor, or 'none'.
-- VARCHAR(16) so backends can be added without a schema change.
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Monitors' AND column_name = 'ObjectDetection') > 0,
"SELECT 'Column ObjectDetection already exists in Monitors'",
"ALTER TABLE `Monitors` ADD COLUMN `ObjectDetection` VARCHAR(16) NOT NULL DEFAULT 'none' AFTER `AnalysisImageOpacity`"
));
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ObjectDetectionModel: path/name of the model file used by the backend.
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Monitors' AND column_name = 'ObjectDetectionModel') > 0,
"SELECT 'Column ObjectDetectionModel already exists in Monitors'",
"ALTER TABLE `Monitors` ADD COLUMN `ObjectDetectionModel` VARCHAR(255) NOT NULL DEFAULT '' AFTER `ObjectDetection`"
));
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ObjectDetectionObjectThreshold: minimum confidence for a detection to count.
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Monitors' AND column_name = 'ObjectDetectionObjectThreshold') > 0,
"SELECT 'Column ObjectDetectionObjectThreshold already exists in Monitors'",
"ALTER TABLE `Monitors` ADD COLUMN `ObjectDetectionObjectThreshold` FLOAT NOT NULL DEFAULT 0.4 AFTER `ObjectDetectionModel`"
));
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ObjectDetectionNMSThreshold: non-maximum-suppression IoU threshold.
SET @s = (SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE()
AND table_name = 'Monitors' AND column_name = 'ObjectDetectionNMSThreshold') > 0,
"SELECT 'Column ObjectDetectionNMSThreshold already exists in Monitors'",
"ALTER TABLE `Monitors` ADD COLUMN `ObjectDetectionNMSThreshold` FLOAT NOT NULL DEFAULT 0.25 AFTER `ObjectDetectionObjectThreshold`"
));
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Widen ObjectDetection to VARCHAR(16) in case an older upgrade path left it as
-- an ENUM or a narrower VARCHAR.
ALTER TABLE `Monitors` MODIFY `ObjectDetection` VARCHAR(16) NOT NULL DEFAULT 'none';
--
-- AI tables
--
-- AI Datasets table - stores datasets like COCO, ImageNet, etc.
CREATE TABLE IF NOT EXISTS `AI_Datasets` (
`Id` int(10) unsigned NOT NULL auto_increment,
`Name` varchar(64) NOT NULL,
`Description` TEXT,
`Version` varchar(32),
`NumClasses` int(10) unsigned NOT NULL,
PRIMARY KEY (`Id`),
UNIQUE KEY `AI_Datasets_Name_idx` (`Name`)
) ENGINE=InnoDB;
-- AI Models table - stores AI model implementations
CREATE TABLE IF NOT EXISTS `AI_Models` (
`Id` int(10) unsigned NOT NULL auto_increment,
`Name` varchar(64) NOT NULL,
`Description` TEXT,
`ModelPath` varchar(255),
`Framework` enum('TensorFlow','PyTorch','ONNX','OpenVINO','TensorRT','Other') NOT NULL default 'ONNX',
`Version` varchar(32),
`DatasetId` int(10) unsigned,
`Enabled` tinyint(1) unsigned NOT NULL default 0,
PRIMARY KEY (`Id`),
UNIQUE KEY `AI_Models_Name_idx` (`Name`),
FOREIGN KEY (`DatasetId`) REFERENCES `AI_Datasets` (`Id`) ON DELETE SET NULL
) ENGINE=InnoDB;
-- AI Object Classes table - stores object classes from datasets
CREATE TABLE IF NOT EXISTS `AI_Object_Classes` (
`Id` int(10) unsigned NOT NULL auto_increment,
`DatasetId` int(10) unsigned NOT NULL,
`ClassName` varchar(64) NOT NULL,
`ClassIndex` int(10) unsigned NOT NULL,
`Description` TEXT,
PRIMARY KEY (`Id`),
KEY `AI_Object_Classes_DatasetId_idx` (`DatasetId`),
UNIQUE KEY `AI_Object_Classes_Dataset_Class_idx` (`DatasetId`, `ClassName`),
FOREIGN KEY (`DatasetId`) REFERENCES `AI_Datasets` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- AI Detection Settings table - stores detection settings per monitor and object class
CREATE TABLE IF NOT EXISTS `AI_Detection_Settings` (
`Id` int(10) unsigned NOT NULL auto_increment,
`MonitorId` int(10) unsigned NULL,
`ObjectClassId` int(10) unsigned NOT NULL,
`Enabled` tinyint(1) unsigned NOT NULL default 1,
`ReportDetection` tinyint(1) unsigned NOT NULL default 1,
`ConfidenceThreshold` tinyint(3) unsigned NOT NULL default 50,
`BoxColor` varchar(7) NOT NULL default '#FF0000',
PRIMARY KEY (`Id`),
KEY `AI_Detection_Settings_MonitorId_idx` (`MonitorId`),
KEY `AI_Detection_Settings_ObjectClassId_idx` (`ObjectClassId`),
UNIQUE KEY `AI_Detection_Settings_Monitor_Object_idx` (`MonitorId`, `ObjectClassId`),
FOREIGN KEY (`MonitorId`) REFERENCES `Monitors` (`Id`) ON DELETE CASCADE,
FOREIGN KEY (`ObjectClassId`) REFERENCES `AI_Object_Classes` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- AI Detections table - stores actual detection results
CREATE TABLE IF NOT EXISTS `AI_Detections` (
`Id` BIGINT unsigned NOT NULL auto_increment,
`EventId` BIGINT unsigned NOT NULL,
`FrameId` BIGINT unsigned,
`ObjectClassId` int(10) unsigned NOT NULL,
`Confidence` decimal(5,4) NOT NULL,
`BoundingBoxX` int(10) unsigned,
`BoundingBoxY` int(10) unsigned,
`BoundingBoxWidth` int(10) unsigned,
`BoundingBoxHeight` int(10) unsigned,
`DetectedAt` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`Id`),
KEY `AI_Detections_EventId_idx` (`EventId`),
KEY `AI_Detections_FrameId_idx` (`FrameId`),
KEY `AI_Detections_ObjectClassId_idx` (`ObjectClassId`),
FOREIGN KEY (`EventId`) REFERENCES `Events` (`Id`) ON DELETE CASCADE,
FOREIGN KEY (`FrameId`) REFERENCES `Frames` (`Id`) ON DELETE SET NULL,
FOREIGN KEY (`ObjectClassId`) REFERENCES `AI_Object_Classes` (`Id`) ON DELETE CASCADE
) ENGINE=InnoDB;
--
-- Seed the COCO 2017 dataset (80 classes) and default detection settings.
--
INSERT IGNORE INTO AI_Datasets (Id, Name, Description, Version, NumClasses) VALUES
(1, 'COCO', 'Microsoft Common Objects in Context', '2017', 80);
INSERT IGNORE INTO AI_Object_Classes (DatasetId, ClassName, ClassIndex, Description) VALUES
(1, 'person', 0, 'Person'),
(1, 'bicycle', 1, 'Bicycle'),
(1, 'car', 2, 'Car'),
(1, 'motorcycle', 3, 'Motorcycle'),
(1, 'airplane', 4, 'Airplane'),
(1, 'bus', 5, 'Bus'),
(1, 'train', 6, 'Train'),
(1, 'truck', 7, 'Truck'),
(1, 'boat', 8, 'Boat'),
(1, 'traffic light', 9, 'Traffic light'),
(1, 'fire hydrant', 10, 'Fire hydrant'),
(1, 'stop sign', 11, 'Stop sign'),
(1, 'parking meter', 12, 'Parking meter'),
(1, 'bench', 13, 'Bench'),
(1, 'bird', 14, 'Bird'),
(1, 'cat', 15, 'Cat'),
(1, 'dog', 16, 'Dog'),
(1, 'horse', 17, 'Horse'),
(1, 'sheep', 18, 'Sheep'),
(1, 'cow', 19, 'Cow'),
(1, 'elephant', 20, 'Elephant'),
(1, 'bear', 21, 'Bear'),
(1, 'zebra', 22, 'Zebra'),
(1, 'giraffe', 23, 'Giraffe'),
(1, 'backpack', 24, 'Backpack'),
(1, 'umbrella', 25, 'Umbrella'),
(1, 'handbag', 26, 'Handbag'),
(1, 'tie', 27, 'Tie'),
(1, 'suitcase', 28, 'Suitcase'),
(1, 'frisbee', 29, 'Frisbee'),
(1, 'skis', 30, 'Skis'),
(1, 'snowboard', 31, 'Snowboard'),
(1, 'sports ball', 32, 'Sports ball'),
(1, 'kite', 33, 'Kite'),
(1, 'baseball bat', 34, 'Baseball bat'),
(1, 'baseball glove', 35, 'Baseball glove'),
(1, 'skateboard', 36, 'Skateboard'),
(1, 'surfboard', 37, 'Surfboard'),
(1, 'tennis racket', 38, 'Tennis racket'),
(1, 'bottle', 39, 'Bottle'),
(1, 'wine glass', 40, 'Wine glass'),
(1, 'cup', 41, 'Cup'),
(1, 'fork', 42, 'Fork'),
(1, 'knife', 43, 'Knife'),
(1, 'spoon', 44, 'Spoon'),
(1, 'bowl', 45, 'Bowl'),
(1, 'banana', 46, 'Banana'),
(1, 'apple', 47, 'Apple'),
(1, 'sandwich', 48, 'Sandwich'),
(1, 'orange', 49, 'Orange'),
(1, 'broccoli', 50, 'Broccoli'),
(1, 'carrot', 51, 'Carrot'),
(1, 'hot dog', 52, 'Hot dog'),
(1, 'pizza', 53, 'Pizza'),
(1, 'donut', 54, 'Donut'),
(1, 'cake', 55, 'Cake'),
(1, 'chair', 56, 'Chair'),
(1, 'couch', 57, 'Couch'),
(1, 'potted plant', 58, 'Potted plant'),
(1, 'bed', 59, 'Bed'),
(1, 'dining table', 60, 'Dining table'),
(1, 'toilet', 61, 'Toilet'),
(1, 'tv', 62, 'TV'),
(1, 'laptop', 63, 'Laptop'),
(1, 'mouse', 64, 'Mouse'),
(1, 'remote', 65, 'Remote'),
(1, 'keyboard', 66, 'Keyboard'),
(1, 'cell phone', 67, 'Cell phone'),
(1, 'microwave', 68, 'Microwave'),
(1, 'oven', 69, 'Oven'),
(1, 'toaster', 70, 'Toaster'),
(1, 'sink', 71, 'Sink'),
(1, 'refrigerator', 72, 'Refrigerator'),
(1, 'book', 73, 'Book'),
(1, 'clock', 74, 'Clock'),
(1, 'vase', 75, 'Vase'),
(1, 'scissors', 76, 'Scissors'),
(1, 'teddy bear', 77, 'Teddy bear'),
(1, 'hair drier', 78, 'Hair drier'),
(1, 'toothbrush', 79, 'Toothbrush');
-- Default detection settings (MonitorId NULL = global default): enable person
-- and common vehicles.
INSERT IGNORE INTO AI_Detection_Settings (MonitorId, ObjectClassId, Enabled, ReportDetection, ConfidenceThreshold, BoxColor)
SELECT NULL, Id, 1, 1,
CASE WHEN ClassName = 'person' THEN 60 ELSE 50 END,
CASE
WHEN ClassName = 'person' THEN '#FF0000'
WHEN ClassName = 'car' THEN '#0000FF'
WHEN ClassName = 'truck' THEN '#0066FF'
WHEN ClassName = 'bus' THEN '#0099FF'
WHEN ClassName = 'motorcycle' THEN '#00CCFF'
ELSE '#808080'
END
FROM AI_Object_Classes
WHERE DatasetId = (SELECT Id FROM AI_Datasets WHERE Name = 'COCO' LIMIT 1)
AND ClassName IN ('person', 'car', 'truck', 'bus', 'motorcycle');
@@ -185,6 +185,9 @@ our %mem_data = (
video_fifo => { type=>'int8[64]', seq=>$mem_seq++ },
audio_fifo => { type=>'int8[64]', seq=>$mem_seq++ },
janus_pin => { type=>'int8[64]', seq=>$mem_seq++ },
last_analysis_index => { type=>'int32', seq=>$mem_seq++ },
analysis_image_count => { type=>'int32', seq=>$mem_seq++ },
analysis_pad => { type=>'uint8[8]', seq=>$mem_seq++ },
}
},
trigger_data => { type=>'TriggerData', seq=>$mem_seq++, 'contents'=> {
+91 -45
View File
@@ -300,7 +300,7 @@ Monitor::Monitor() :
shared_timestamps(nullptr),
shared_images(nullptr),
image_pixelformats(nullptr),
alarm_image_pixelformat(nullptr),
analysis_image_pixelformats(nullptr),
shm_slot_size(0),
video_stream_id(-1),
audio_stream_id(-1),
@@ -1016,9 +1016,9 @@ bool Monitor::connect() {
+ sizeof(VideoStoreData) //Information to pass back to the capture process
+ (image_buffer_count*sizeof(struct timeval))
+ (image_buffer_count*image_size)
+ (image_buffer_count*image_size) // alarm_images
+ (image_buffer_count*image_size) // analysis image ring (alarm_images)
+ (image_buffer_count*sizeof(AVPixelFormat)) // per-slot capture pix fmt
+ sizeof(AVPixelFormat) // alarm_image pix fmt (cross-process sync)
+ (image_buffer_count*sizeof(AVPixelFormat)) // per-slot analysis pix fmt (cross-process sync)
// Padding covers two independent alignment adjustments:
// * up to 63 bytes to push shared_images to a 64-byte boundary
// * up to alignof(AVPixelFormat)-1 bytes to push
@@ -1156,23 +1156,28 @@ bool Monitor::connect() {
&(shared_images[i*image_size]), image_size, 0);
image_buffer[i]->HoldBuffer(true); /* Don't release the internal buffer or replace it with another */
}
// alarm_image follows the per-slot format convention. Initial format is a
// placeholder; consumers should sync via GetAlarmImage() which reads the
// cross-process *alarm_image_pixelformat.
alarm_image.AssignDirect(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P,
&(shared_images[image_buffer_count*image_size]), image_size, ZM_BUFTYPE_DONTFREE);
alarm_image.HoldBuffer(true); /* Don't release the internal buffer or replace it with another */
if (alarm_image.Buffer() + image_size > mem_ptr + mem_size) {
Warning("We will exceed memsize by %td bytes!", (alarm_image.Buffer() + image_size) - (mem_ptr + mem_size));
// Analysis image ring: one Image per slot in the alarm_images SHM region
// (the second image_buffer_count*image_size block). Each slot follows the
// per-slot format convention; consumers sync via GetAlarmImage() which reads
// the cross-process analysis_image_pixelformats[last_analysis_index].
analysis_image_buffer.resize(image_buffer_count);
for (int32_t i = 0; i < image_buffer_count; i++) {
analysis_image_buffer[i] = new Image(width, height, ZM_COLOUR_YUV420P, ZM_SUBPIX_ORDER_YUV420P,
&(shared_images[(image_buffer_count+i)*image_size]), image_size, 0);
analysis_image_buffer[i]->HoldBuffer(true); /* Don't release the internal buffer or replace it with another */
}
if (analysis_image_buffer[image_buffer_count-1]->Buffer() + image_size > mem_ptr + mem_size) {
Warning("We will exceed memsize by %td bytes!",
(analysis_image_buffer[image_buffer_count-1]->Buffer() + image_size) - (mem_ptr + mem_size));
}
// Layout in SHM is: image_buffer_count*image_size for image_buffer slots,
// then image_buffer_count*image_size for alarm_image slots, THEN the
// image_pixelformats[image_buffer_count] array, THEN the single
// alarm_image_pixelformat slot. Placing image_pixelformats at
// +1*image_buffer_count*image_size would collide with the alarm_image
// buffer region — zmc's writes to image_pixelformats[index] would corrupt
// the alarm image, and zms would read alarm-image bytes back as
// AVPixelFormat enum values, producing per-frame garble.
// then image_buffer_count*image_size for analysis_image_buffer slots, THEN
// the image_pixelformats[image_buffer_count] array, THEN the
// analysis_image_pixelformats[image_buffer_count] array. Placing
// image_pixelformats at +1*image_buffer_count*image_size would collide with
// the analysis image buffer region — zmc's writes to image_pixelformats[index]
// would corrupt the analysis image, and zms would read analysis-image bytes
// back as AVPixelFormat enum values, producing per-frame garble.
//
// image_size may not be a multiple of alignof(AVPixelFormat) (for
// example GRAY8 with odd width when image_size comes from
@@ -1187,7 +1192,7 @@ bool Monitor::connect() {
const uintptr_t pixfmt_align = alignof(AVPixelFormat);
pixfmt_addr = (pixfmt_addr + pixfmt_align - 1) & ~(pixfmt_align - 1);
image_pixelformats = reinterpret_cast<AVPixelFormat *>(pixfmt_addr);
alarm_image_pixelformat = image_pixelformats + image_buffer_count;
analysis_image_pixelformats = image_pixelformats + image_buffer_count;
if (purpose == CAPTURE) {
memset(mem_ptr, 0, mem_size);
@@ -1203,6 +1208,8 @@ bool Monitor::connect() {
shared_data->state = state = IDLE;
shared_data->last_write_index = image_buffer_count;
shared_data->last_read_index = image_buffer_count;
shared_data->last_analysis_index = image_buffer_count; // sentinel: nothing published yet
shared_data->analysis_image_count = 0;
shared_data->last_write_time = 0;
shared_data->last_event_id = 0;
shared_data->action = (Action)0;
@@ -1219,8 +1226,8 @@ bool Monitor::connect() {
// for "format not yet published". Initialise explicitly.
for (int32_t i = 0; i < image_buffer_count; i++) {
image_pixelformats[i] = AV_PIX_FMT_NONE;
analysis_image_pixelformats[i] = AV_PIX_FMT_NONE;
}
*alarm_image_pixelformat = AV_PIX_FMT_NONE;
shared_data->alarm_cause[0] = 0;
shared_data->video_fifo_path[0] = 0;
shared_data->audio_fifo_path[0] = 0;
@@ -1315,7 +1322,6 @@ bool Monitor::disconnect() {
return true;
}
alarm_image.HoldBuffer(false); /* Allow to reset buffer when we connect */
if (purpose == CAPTURE) {
if (unlink(mem_file.c_str()) < 0) {
Warning("Can't unlink '%s': %s", mem_file.c_str(), strerror(errno));
@@ -1356,6 +1362,10 @@ bool Monitor::disconnect() {
// We delete the image because it is an object pointing to space that won't be free'd.
delete image_buffer[i];
image_buffer[i] = nullptr;
// analysis_image_buffer entries point into the same SHM mapping (with
// HoldBuffer set) so deleting them won't free the SHM bytes.
delete analysis_image_buffer[i];
analysis_image_buffer[i] = nullptr;
}
return true;
@@ -1418,56 +1428,70 @@ void Monitor::AddPrivacyBitmask() {
}
Image *Monitor::GetAlarmImage() {
// alarm_image's bytes live in SHM and are written by the capture/analysis
// process; alarm_image_pixelformat carries the format that process used.
// Without this sync, a reader process (zms) would interpret the bytes
// with whatever placeholder format alarm_image was constructed with —
// Return the most recently published analysis-ring slot. The ring bytes live
// in SHM and are written by the capture/analysis process;
// analysis_image_pixelformats[index] carries the format that process used.
// Without this sync, a reader process (zms) would interpret the bytes with
// whatever placeholder format the slot Image was constructed with —
// producing garbled output whenever the writer used RGB24/RGBA/etc.
if (alarm_image_pixelformat != nullptr) {
AVPixelFormat fmt = *alarm_image_pixelformat;
if (fmt != AV_PIX_FMT_NONE && alarm_image.PixFormat() != fmt) {
int32_t index = shared_data->last_analysis_index;
// Sentinel (image_buffer_count) or an out-of-range value means "nothing
// published yet" — fall back to slot 0 so callers still get a valid Image.
if (index < 0 || index >= image_buffer_count) index = 0;
Image *img = analysis_image_buffer[index];
if (analysis_image_pixelformats != nullptr) {
AVPixelFormat fmt = analysis_image_pixelformats[index];
if (fmt != AV_PIX_FMT_NONE && img->PixFormat() != fmt) {
unsigned int probe_colours, probe_subpix;
if (!zm_colours_from_pixformat(fmt, probe_colours, probe_subpix)) {
Warning("GetAlarmImage: ignoring unsupported pixelformat %d; keeping current %s",
fmt, zm_get_pix_fmt_name(alarm_image.PixFormat()));
fmt, zm_get_pix_fmt_name(img->PixFormat()));
} else {
int required = av_image_get_buffer_size(fmt, width, height, 32);
if (required < 0 || static_cast<size_t>(required) > shm_slot_size) {
Warning("GetAlarmImage: format %s requires %d bytes but slot capacity is %zu; "
"keeping current %s",
zm_get_pix_fmt_name(fmt), required, shm_slot_size,
zm_get_pix_fmt_name(alarm_image.PixFormat()));
zm_get_pix_fmt_name(img->PixFormat()));
} else {
alarm_image.AVPixFormat(fmt);
img->AVPixFormat(fmt);
}
}
}
}
return &alarm_image;
return img;
}
void Monitor::WriteAlarmImage(const Image &src) {
// Mirror WriteShmFrame's contract for alarm_image: copy bytes then
// publish the canonical AVPixelFormat so reader processes can interpret
// the SHM correctly via GetAlarmImage().
// Publish src into the next analysis-ring slot. Mirror WriteShmFrame's
// contract: copy bytes then publish the canonical AVPixelFormat so reader
// processes can interpret the SHM correctly via GetAlarmImage().
//
// Only publish *alarm_image_pixelformat if Assign actually adopted the
// source format. Image::Assign silently leaves the destination untouched
// on failure (held-buffer undersize, unknown src format), so publishing
// a new format whose bytes never landed would make readers misinterpret
// the previous alarm-image contents.
// The slot is chosen from analysis_image_count so successive writes rotate
// through the ring. last_analysis_index is published LAST (after the bytes
// and format are in place) so a reader that samples last_analysis_index
// always sees a fully written slot.
//
// Only publish the format if Assign actually adopted the source format.
// Image::Assign silently leaves the destination untouched on failure
// (held-buffer undersize, unknown src format), so publishing a new format
// whose bytes never landed would make readers misinterpret the slot.
int32_t index = shared_data->analysis_image_count % image_buffer_count;
const AVPixelFormat src_fmt = src.PixFormat();
alarm_image.Assign(src);
if (alarm_image_pixelformat != nullptr) {
if (alarm_image.PixFormat() == src_fmt) {
*alarm_image_pixelformat = src_fmt;
Image *dst = analysis_image_buffer[index];
dst->Assign(src);
if (analysis_image_pixelformats != nullptr) {
if (dst->PixFormat() == src_fmt) {
analysis_image_pixelformats[index] = src_fmt;
} else {
Warning("WriteAlarmImage: assign failed (dst fmt %s != src fmt %s); "
"keeping previously published pixelformat",
zm_get_pix_fmt_name(alarm_image.PixFormat()),
zm_get_pix_fmt_name(dst->PixFormat()),
zm_get_pix_fmt_name(src_fmt));
}
}
shared_data->last_analysis_index = index;
shared_data->analysis_image_count++;
}
int Monitor::GetImage(int32_t index, int scale) {
@@ -3432,6 +3456,28 @@ bool Monitor::Decode() {
}
}
// Capture paths that deliver a raw Image without an ffmpeg decode (e.g.
// LocalCamera/V4L2) leave packet->in_frame null even though the pixels are
// already present. Wrap the image's planes in an AVFrame — no copy, just
// pointers via PopulateFrame — so in_frame consumers work without a real
// decode step. Any format the Image supports is fine; consumers that need a
// specific layout check for themselves (get_y_image, for instance, rejects
// RGB with a precise "no Y plane" message rather than "no frame").
//
// Done here, after PHASE 5, so the frame reflects the oriented/masked image
// (we don't re-run orientation on a shared Y plane), and after the codec
// phases so transfer_hwframe is never called with a null codec context.
// videostore is unaffected: it prefers packet->image for frame data and
// always derives pts from packet->timestamp, never in_frame->pts.
if (packet->image && !packet->in_frame) {
av_frame_ptr synth{av_frame_alloc()};
if (synth && (packet->image->PopulateFrame(synth.get()) >= 0)) {
packet->in_frame = std::move(synth);
Debug(2, "Synthesized in_frame from image for packet %d (%s)",
packet->image_index, av_get_pix_fmt_name(packet->image->PixFormat()));
}
}
packet->decoded = true;
packet->notify_all();
packetqueue.notify_all(); // Wake up analysis thread waiting for decoded packets
+29 -3
View File
@@ -246,8 +246,27 @@ class Monitor : public std::enable_shared_from_this<Monitor> {
char video_fifo_path[64]; /* +672 */
char audio_fifo_path[64]; /* +736 */
char janus_pin[64]; /* +800 */
/* 864 total */
/* Analysis image ring: the annotated/analysis image is published into a
* ring of image_buffer_count slots (reusing the alarm_images SHM region).
* last_analysis_index is the slot most recently written (or
* image_buffer_count as the "nothing written yet" sentinel);
* analysis_image_count is a monotonic counter of analysis images published.
* Appended at the end so no earlier SharedData offset shifts. */
int32_t last_analysis_index; /* +864 */
int32_t analysis_image_count; /* +868 */
uint32_t analysis_pad[2]; /* +872 keep 16-byte multiple */
/* 880 total */
} SharedData;
// Cross-process ABI guard: zmc/zma/zms plus the Perl (Memory.pm) and PHP
// (Monitor.php) SHM readers all assume this exact layout. If it changes,
// update those readers in lockstep and bump the size below.
// Cross-process ABI guard. The struct is naturally aligned (NOT packed), so
// two 4-byte pads exist (before capture_fps and before the startup_time
// union); the /* +N */ comments above are the packed-layout ideal and do NOT
// reflect real offsets. zmc/zma/zms and the Perl (Memory.pm, which computes
// alignment) SHM reader assume this exact layout. If it changes, update the
// readers in lockstep and bump the size here.
static_assert(sizeof(SharedData) == 888, "SharedData layout changed; update Memory.pm and Monitor.php offsets");
enum TriggerState : uint32 {
TRIGGER_CANCEL,
@@ -659,7 +678,10 @@ class Monitor : public std::enable_shared_from_this<Monitor> {
unsigned char *shared_images;
std::vector<Image *> image_buffer;
AVPixelFormat *image_pixelformats;
AVPixelFormat *alarm_image_pixelformat; // cross-process format for alarm_image
// Per-slot cross-process format for the analysis image ring (one entry per
// analysis_image_buffer slot), mirroring image_pixelformats for the capture
// ring. Replaces the former single alarm_image_pixelformat.
AVPixelFormat *analysis_image_pixelformats;
size_t shm_slot_size; // per-slot byte capacity, sized to RGBA upper bound
int video_stream_id; // will be filled in PrimeCapture
@@ -705,7 +727,11 @@ class Monitor : public std::enable_shared_from_this<Monitor> {
Image delta_image;
Image ref_image;
Image alarm_image; // Used in creating analysis images, will be initialized in Analysis
// Analysis image ring: the annotated/analysis image is published into a ring
// of image_buffer_count slots living in the alarm_images SHM region. Readers
// pick up the newest via shared_data->last_analysis_index. Replaces the
// former single alarm_image.
std::vector<Image *> analysis_image_buffer;
Image write_image; // Used when creating snapshot images
std::string diag_path_ref;
std::string diag_path_delta;
+1 -1
View File
@@ -1 +1 @@
1.39.16
1.39.17
+83
View File
@@ -0,0 +1,83 @@
<?php
// This is the HTML representing the AI Object Class modal from Options -> AI Classes
if ( !isset($_REQUEST['id']) ) {
ajaxError('Class Id Not Provided');
return;
}
$result = '';
$checked = ' checked="checked"';
$null = '';
$cid = validCardinal($_REQUEST['id']);
if ( !canEdit('System') ) return;
if ( $cid ) {
$class = dbFetchOne('SELECT * FROM AI_Object_Classes WHERE Id=?', NULL, [$cid]);
if ( !$class ) return;
} else {
$class = array(
'Id' => 0,
'DatasetId' => null,
'ClassName' => '',
'ClassIndex' => 0,
'Description' => ''
);
}
// Get datasets for dropdown
$datasets = array();
$result = dbQuery('SELECT Id, Name FROM AI_Datasets ORDER BY Name');
if ($result) {
while ($row = dbFetchNext($result)) {
$datasets[$row['Id']] = $row['Name'];
}
}
?>
<div class="modal fade" id="ClassModal" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo translate('AIObjectClass') .' - '. validHtmlStr($class['ClassName']) ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="classModalForm" name="contentForm" method="post" action="?view=ai_class" class="validateFormOnSubmit">
<div class="modal-body">
<?php
// We have to manually insert the csrf key into the form when using a modal generated via ajax call
echo getCSRFinputHTML();
?>
<input type="hidden" name="object" value="ai_class"/>
<input type="hidden" name="id" value="<?php echo $cid ?>"/>
<table class="table-sm">
<tbody>
<tr class="Dataset">
<th scope="row"><?php echo translate('Dataset') ?></th>
<td><?php echo htmlSelect('newClass[DatasetId]', $datasets, $class['DatasetId']) ?></td>
</tr>
<tr class="ClassName">
<th scope="row"><?php echo translate('ClassName') ?></th>
<td><input type="text" name="newClass[ClassName]" value="<?php echo validHtmlStr($class['ClassName']) ?>" required/></td>
</tr>
<tr class="ClassIndex">
<th scope="row"><?php echo translate('ClassIndex') ?></th>
<td><input type="number" name="newClass[ClassIndex]" value="<?php echo validCardinal($class['ClassIndex']) ?>" step="1" min="0" required/></td>
</tr>
<tr class="Description">
<th scope="row"><?php echo translate('Description') ?></th>
<td><textarea name="newClass[Description]" rows="3"><?php echo validHtmlStr($class['Description']) ?></textarea></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button name="action" id="classSubmitBtn" type="submit" class="btn btn-primary" value="save"><?php echo translate('Save') ?></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php echo translate('Cancel') ?></button>
</div>
</form>
</div>
</div>
</div>
+74
View File
@@ -0,0 +1,74 @@
<?php
// This is the HTML representing the AI Dataset modal from Options -> AI Datasets
if ( !isset($_REQUEST['id']) ) {
ajaxError('Dataset Id Not Provided');
return;
}
$result = '';
$checked = ' checked="checked"';
$null = '';
$did = validCardinal($_REQUEST['id']);
if ( !canEdit('System') ) return;
if ( $did ) {
$dataset = dbFetchOne('SELECT * FROM AI_Datasets WHERE Id=?', NULL, [$did]);
if ( !$dataset ) return;
} else {
$dataset = array(
'Id' => 0,
'Name' => '',
'Description' => '',
'Version' => '',
'NumClasses' => 0
);
}
?>
<div class="modal fade" id="DatasetModal" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo translate('AIDataset') .' - '. validHtmlStr($dataset['Name']) ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="datasetModalForm" name="contentForm" method="post" action="?view=ai_dataset" class="validateFormOnSubmit">
<div class="modal-body">
<?php
// We have to manually insert the csrf key into the form when using a modal generated via ajax call
echo getCSRFinputHTML();
?>
<input type="hidden" name="object" value="ai_dataset"/>
<input type="hidden" name="id" value="<?php echo $did ?>"/>
<table class="table-sm">
<tbody>
<tr class="Name">
<th scope="row"><?php echo translate('Name') ?></th>
<td><input type="text" name="newDataset[Name]" value="<?php echo validHtmlStr($dataset['Name']) ?>" required/></td>
</tr>
<tr class="Version">
<th scope="row"><?php echo translate('Version') ?></th>
<td><input type="text" name="newDataset[Version]" value="<?php echo validHtmlStr($dataset['Version']) ?>"/></td>
</tr>
<tr class="NumClasses">
<th scope="row"><?php echo translate('NumClasses') ?></th>
<td><input type="number" name="newDataset[NumClasses]" value="<?php echo validCardinal($dataset['NumClasses']) ?>" step="1" min="0" required/></td>
</tr>
<tr class="Description">
<th scope="row"><?php echo translate('Description') ?></th>
<td><textarea name="newDataset[Description]" rows="3"><?php echo validHtmlStr($dataset['Description']) ?></textarea></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button name="action" id="datasetSubmitBtn" type="submit" class="btn btn-primary" value="save"><?php echo translate('Save') ?></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php echo translate('Cancel') ?></button>
</div>
</form>
</div>
</div>
</div>
+110
View File
@@ -0,0 +1,110 @@
<?php
// This is the HTML representing the AI Model modal from Options -> AI Models
if ( !isset($_REQUEST['id']) ) {
ajaxError('Model Id Not Provided');
return;
}
$result = '';
$checked = ' checked="checked"';
$null = '';
$mid = validCardinal($_REQUEST['id']);
if ( !canEdit('System') ) return;
if ( $mid ) {
$model = dbFetchOne('SELECT * FROM AI_Models WHERE Id=?', NULL, [$mid]);
if ( !$model ) return;
} else {
$model = array(
'Id' => 0,
'Name' => '',
'Description' => '',
'ModelPath' => '',
'Framework' => 'ONNX',
'Version' => '',
'DatasetId' => null,
'Enabled' => 0
);
}
// Get datasets for dropdown
$datasets = array('' => translate('None'));
$result = dbQuery('SELECT Id, Name FROM AI_Datasets ORDER BY Name');
if ($result) {
while ($row = dbFetchNext($result)) {
$datasets[$row['Id']] = $row['Name'];
}
}
$framework_options = array(
'TensorFlow' => 'TensorFlow',
'PyTorch' => 'PyTorch',
'ONNX' => 'ONNX',
'OpenVINO' => 'OpenVINO',
'TensorRT' => 'TensorRT',
'Other' => 'Other'
);
?>
<div class="modal fade" id="ModelModal" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><?php echo translate('AIModel') .' - '. validHtmlStr($model['Name']) ?></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="modelModalForm" name="contentForm" method="post" action="?view=ai_model" class="validateFormOnSubmit">
<div class="modal-body">
<?php
// We have to manually insert the csrf key into the form when using a modal generated via ajax call
echo getCSRFinputHTML();
?>
<input type="hidden" name="object" value="ai_model"/>
<input type="hidden" name="id" value="<?php echo $mid ?>"/>
<table class="table-sm">
<tbody>
<tr class="Name">
<th scope="row"><?php echo translate('Name') ?></th>
<td><input type="text" name="newModel[Name]" value="<?php echo validHtmlStr($model['Name']) ?>" required/></td>
</tr>
<tr class="Framework">
<th scope="row"><?php echo translate('Framework') ?></th>
<td><?php echo htmlSelect('newModel[Framework]', $framework_options, $model['Framework']) ?></td>
</tr>
<tr class="Version">
<th scope="row"><?php echo translate('Version') ?></th>
<td><input type="text" name="newModel[Version]" value="<?php echo validHtmlStr($model['Version']) ?>"/></td>
</tr>
<tr class="Dataset">
<th scope="row"><?php echo translate('Dataset') ?></th>
<td><?php echo htmlSelect('newModel[DatasetId]', $datasets, $model['DatasetId']) ?></td>
</tr>
<tr class="ModelPath">
<th scope="row"><?php echo translate('ModelPath') ?></th>
<td><input type="text" name="newModel[ModelPath]" value="<?php echo validHtmlStr($model['ModelPath']) ?>"/></td>
</tr>
<tr class="Enabled">
<th scope="row"><?php echo translate('Enabled') ?></th>
<td>
<input type="radio" name="newModel[Enabled]" value="1" <?php echo $model['Enabled'] ? $checked : $null ?>/> <?php echo translate('Yes') ?>
<input type="radio" name="newModel[Enabled]" value="0" <?php echo $model['Enabled'] ? $null : $checked ?>/> <?php echo translate('No') ?>
</td>
</tr>
<tr class="Description">
<th scope="row"><?php echo translate('Description') ?></th>
<td><textarea name="newModel[Description]" rows="3"><?php echo validHtmlStr($model['Description']) ?></textarea></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button name="action" id="modelSubmitBtn" type="submit" class="btn btn-primary" value="save"><?php echo translate('Save') ?></button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"><?php echo translate('Cancel') ?></button>
</div>
</form>
</div>
</div>
</div>
+59 -44
View File
@@ -14,57 +14,72 @@ class Monitor extends ZM_Object {
private $shm_id = null;
private $connected = false;
// These offsets are the REAL (naturally-aligned) byte offsets of the C++
// SharedData/TriggerData structs, not the packed-layout ideal implied by the
// struct's /* +N */ comments. The C++ struct is not packed, so the compiler
// inserts a 4-byte pad before capture_fps (after state) and another before
// the startup_time union (after audio_channels); every field from capture_fps
// onward therefore sits 4 or 8 bytes later than the naive packed offset. With
// the analysis image ring counters appended, SharedData is 888 bytes and
// TriggerData starts at 888. The authoritative source is the same alignment
// computation ZoneMinder::Memory (Memory.pm) performs; a
// sizeof(SharedData)==888 static_assert in zm_monitor.h guards the layout.
private $shm_offsets = ['SharedData' => [
'size' => [ 'type'=>'uint32', 'offset'=>0, 'size'=>4 ],
'last_write_index' => [ 'type'=>'int32', 'offset'=>4, 'size'=>4 ],
'last_read_index' => [ 'type'=>'int32', 'offset'=>8, 'size'=>4 ],
'image_count' => [ 'type'=>'int32', 'offset'=>12, 'size'=>4 ],
'state' => [ 'type'=>'uint32', 'offset'=>16, 'size'=>4 ],
'capture_fps' => [ 'type'=>'double', 'offset'=>20, 'size'=>8 ],
'analysis_fps' => [ 'type'=>'double', 'offset'=>28, 'size'=>8 ],
'latitude' => [ 'type'=>'double', 'offset'=>36, 'size'=>8 ],
'longitude' => [ 'type'=>'double', 'offset'=>44, 'size'=>8 ],
'last_event' => [ 'type'=>'uint64', 'offset'=>52, 'size'=>8 ],
'action' => [ 'type'=>'uint32', 'offset'=>60, 'size'=>4 ],
'brightness' => [ 'type'=>'int32', 'offset'=>64, 'size'=>4 ],
'hue' => [ 'type'=>'int32', 'offset'=>68, 'size'=>4 ],
'colour' => [ 'type'=>'int32', 'offset'=>72, 'size'=>4 ],
'contrast' => [ 'type'=>'int32', 'offset'=>76, 'size'=>4 ],
'alarm_x' => [ 'type'=>'int32', 'offset'=>80, 'size'=>4 ],
'alarm_y' => [ 'type'=>'int32', 'offset'=>84, 'size'=>4 ],
'valid' => [ 'type'=>'uint8', 'offset'=>88, 'size'=>1 ],
'capturing' => [ 'type'=>'uint8', 'offset'=>89, 'size'=>1 ],
'analysing' => [ 'type'=>'uint8', 'offset'=>90, 'size'=>1 ],
'recording' => [ 'type'=>'uint8', 'offset'=>91, 'size'=>1 ],
'signal' => [ 'type'=>'uint8', 'offset'=>92, 'size'=>1 ],
'format' => [ 'type'=>'uint8', 'offset'=>93, 'size'=>1 ],
'reserved1' => [ 'type'=>'uint8', 'offset'=>94, 'size'=>1 ],
'reserved2' => [ 'type'=>'uint8', 'offset'=>95, 'size'=>1 ],
'imagesize' => [ 'type'=>'uint32', 'offset'=>96, 'size'=>4 ],
'last_frame_score' => [ 'type'=>'uint32', 'offset'=>100, 'size'=>4 ],
'audio_frequency' => [ 'type'=>'uint32', 'offset'=>104, 'size'=>4 ],
'audio_channels' => [ 'type'=>'uint32', 'offset'=>108, 'size'=>4 ],
'startup_time' => [ 'type'=>'time_t64', 'offset'=>112, 'size'=>8 ],
'heartbeat_time' => [ 'type'=>'time_t64', 'offset'=>120, 'size'=>8 ],
'last_write_time' => [ 'type'=>'time_t64', 'offset'=>128, 'size'=>8 ],
'last_read_time' => [ 'type'=>'time_t64', 'offset'=>136, 'size'=>8 ],
'last_viewed_time' => [ 'type'=>'time_t64', 'offset'=>144, 'size'=>8 ],
'last_analysis_viewed_time' => [ 'type'=>'time_t64', 'offset'=>152, 'size'=>8 ],
'control_state' => [ 'type'=>'uint8[256]', 'offset'=>160, 'size'=>256 ],
'alarm_cause' => [ 'type'=>'int8[256]', 'offset'=>416, 'size'=>256 ],
'video_fifo' => [ 'type'=>'int8[64]', 'offset'=>672, 'size'=>64 ],
'audio_fifo' => [ 'type'=>'int8[64]', 'offset'=>736, 'size'=>64 ],
'janus_pin' => [ 'type'=>'int8[64]', 'offset'=>800, 'size'=>64 ],
],
'capture_fps' => [ 'type'=>'double', 'offset'=>24, 'size'=>8 ],
'analysis_fps' => [ 'type'=>'double', 'offset'=>32, 'size'=>8 ],
'latitude' => [ 'type'=>'double', 'offset'=>40, 'size'=>8 ],
'longitude' => [ 'type'=>'double', 'offset'=>48, 'size'=>8 ],
'last_event' => [ 'type'=>'uint64', 'offset'=>56, 'size'=>8 ],
'action' => [ 'type'=>'uint32', 'offset'=>64, 'size'=>4 ],
'brightness' => [ 'type'=>'int32', 'offset'=>68, 'size'=>4 ],
'hue' => [ 'type'=>'int32', 'offset'=>72, 'size'=>4 ],
'colour' => [ 'type'=>'int32', 'offset'=>76, 'size'=>4 ],
'contrast' => [ 'type'=>'int32', 'offset'=>80, 'size'=>4 ],
'alarm_x' => [ 'type'=>'int32', 'offset'=>84, 'size'=>4 ],
'alarm_y' => [ 'type'=>'int32', 'offset'=>88, 'size'=>4 ],
'valid' => [ 'type'=>'uint8', 'offset'=>92, 'size'=>1 ],
'capturing' => [ 'type'=>'uint8', 'offset'=>93, 'size'=>1 ],
'analysing' => [ 'type'=>'uint8', 'offset'=>94, 'size'=>1 ],
'recording' => [ 'type'=>'uint8', 'offset'=>95, 'size'=>1 ],
'signal' => [ 'type'=>'uint8', 'offset'=>96, 'size'=>1 ],
'format' => [ 'type'=>'uint8', 'offset'=>97, 'size'=>1 ],
'reserved1' => [ 'type'=>'uint8', 'offset'=>98, 'size'=>1 ],
'reserved2' => [ 'type'=>'uint8', 'offset'=>99, 'size'=>1 ],
'imagesize' => [ 'type'=>'uint32', 'offset'=>100, 'size'=>4 ],
'last_frame_score' => [ 'type'=>'uint32', 'offset'=>104, 'size'=>4 ],
'audio_frequency' => [ 'type'=>'uint32', 'offset'=>108, 'size'=>4 ],
'audio_channels' => [ 'type'=>'uint32', 'offset'=>112, 'size'=>4 ],
'startup_time' => [ 'type'=>'time_t64', 'offset'=>120, 'size'=>8 ],
'heartbeat_time' => [ 'type'=>'time_t64', 'offset'=>128, 'size'=>8 ],
'last_write_time' => [ 'type'=>'time_t64', 'offset'=>136, 'size'=>8 ],
'last_read_time' => [ 'type'=>'time_t64', 'offset'=>144, 'size'=>8 ],
'last_viewed_time' => [ 'type'=>'time_t64', 'offset'=>152, 'size'=>8 ],
'last_analysis_viewed_time' => [ 'type'=>'time_t64', 'offset'=>160, 'size'=>8 ],
'control_state' => [ 'type'=>'uint8[256]', 'offset'=>168, 'size'=>256 ],
'alarm_cause' => [ 'type'=>'int8[256]', 'offset'=>424, 'size'=>256 ],
'video_fifo' => [ 'type'=>'int8[64]', 'offset'=>680, 'size'=>64 ],
'audio_fifo' => [ 'type'=>'int8[64]', 'offset'=>744, 'size'=>64 ],
'janus_pin' => [ 'type'=>'int8[64]', 'offset'=>808, 'size'=>64 ],
// Analysis image ring counters, appended at the end of SharedData followed
// by 8 bytes of padding (kept for the 16-byte-multiple layout invariant),
// so SharedData is now 888 bytes and TriggerData starts at 888.
'last_analysis_index' => [ 'type'=>'int32', 'offset'=>872, 'size'=>4 ],
'analysis_image_count' => [ 'type'=>'int32', 'offset'=>876, 'size'=>4 ],
],
'TriggerData' => [
'size' => [ 'type'=>'uint32', 'offset'=>864, 'size'=>4 ],
'state' => [ 'type'=>'uint32', 'offset'=>868, 'size'=>4 ],
'score' => [ 'type'=>'uint32', 'offset'=>872, 'size'=>4 ],
'padding' => [ 'type'=>'uint32', 'offset'=>876, 'size'=>4 ],
'cause' => [ 'type'=>'int8[32]', 'offset'=>880, 'size'=>32 ],
'text' => [ 'type'=>'int8[256]', 'offset'=>912, 'size'=>256 ],
'showtext' => [ 'type'=>'int8[256]', 'offset'=>1168, 'size'=>256 ],
// 1424
'size' => [ 'type'=>'uint32', 'offset'=>888, 'size'=>4 ],
'state' => [ 'type'=>'uint32', 'offset'=>892, 'size'=>4 ],
'score' => [ 'type'=>'uint32', 'offset'=>896, 'size'=>4 ],
'padding' => [ 'type'=>'uint32', 'offset'=>900, 'size'=>4 ],
'cause' => [ 'type'=>'int8[32]', 'offset'=>904, 'size'=>32 ],
'text' => [ 'type'=>'int8[256]', 'offset'=>936, 'size'=>256 ],
'showtext' => [ 'type'=>'int8[256]', 'offset'=>1192, 'size'=>256 ],
// 1448
]
];
+58
View File
@@ -0,0 +1,58 @@
<?php
//
// ZoneMinder web action file
// Copyright (C) 2024 ZoneMinder LLC
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// AI Object Class edit actions
if (!canEdit('System')) {
ZM\Warning('Need System permissions to manage AI object classes');
return;
}
if ($action == 'save') {
if (!empty($_REQUEST['id'])) {
$dbClass = dbFetchOne('SELECT * FROM AI_Object_Classes WHERE Id=?', NULL, [$_REQUEST['id']]);
} else {
$dbClass = array();
}
$types = array();
$changes = getFormChanges($dbClass, $_REQUEST['newClass'], $types);
if (count($changes)) {
if (!empty($_REQUEST['id'])) {
dbQuery('UPDATE AI_Object_Classes SET '.implode(', ', $changes).' WHERE Id = ?',
array($_REQUEST['id']) );
} else {
dbQuery('INSERT INTO AI_Object_Classes SET '.implode(', ', $changes));
}
$refreshParent = true;
}
$redirect = '?view=options&tab=ai_classes';
} else if ($action == 'delete') {
if ( !empty($_REQUEST['markIds']) ) {
foreach ($_REQUEST['markIds'] as $Id) {
dbQuery('DELETE FROM AI_Object_Classes WHERE Id=?', array($Id));
}
$refreshParent = true;
}
$redirect = '?view=options&tab=ai_classes';
} else {
ZM\Error("Unknown action $action in saving AI Object Class");
}
?>
+58
View File
@@ -0,0 +1,58 @@
<?php
//
// ZoneMinder web action file
// Copyright (C) 2024 ZoneMinder LLC
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// AI Dataset edit actions
if (!canEdit('System')) {
ZM\Warning('Need System permissions to manage AI datasets');
return;
}
if ($action == 'save') {
if (!empty($_REQUEST['id'])) {
$dbDataset = dbFetchOne('SELECT * FROM AI_Datasets WHERE Id=?', NULL, [$_REQUEST['id']]);
} else {
$dbDataset = array();
}
$types = array();
$changes = getFormChanges($dbDataset, $_REQUEST['newDataset'], $types);
if (count($changes)) {
if (!empty($_REQUEST['id'])) {
dbQuery('UPDATE AI_Datasets SET '.implode(', ', $changes).' WHERE Id = ?',
array($_REQUEST['id']) );
} else {
dbQuery('INSERT INTO AI_Datasets SET '.implode(', ', $changes));
}
$refreshParent = true;
}
$redirect = '?view=options&tab=ai_datasets';
} else if ($action == 'delete') {
if ( !empty($_REQUEST['markIds']) ) {
foreach ($_REQUEST['markIds'] as $Id) {
dbQuery('DELETE FROM AI_Datasets WHERE Id=?', array($Id));
}
$refreshParent = true;
}
$redirect = '?view=options&tab=ai_datasets';
} else {
ZM\Error("Unknown action $action in saving AI Dataset");
}
?>
+58
View File
@@ -0,0 +1,58 @@
<?php
//
// ZoneMinder web action file
// Copyright (C) 2024 ZoneMinder LLC
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// AI Model edit actions
if (!canEdit('System')) {
ZM\Warning('Need System permissions to manage AI models');
return;
}
if ($action == 'save') {
if (!empty($_REQUEST['id'])) {
$dbModel = dbFetchOne('SELECT * FROM AI_Models WHERE Id=?', NULL, [$_REQUEST['id']]);
} else {
$dbModel = array();
}
$types = array();
$changes = getFormChanges($dbModel, $_REQUEST['newModel'], $types);
if (count($changes)) {
if (!empty($_REQUEST['id'])) {
dbQuery('UPDATE AI_Models SET '.implode(', ', $changes).' WHERE Id = ?',
array($_REQUEST['id']) );
} else {
dbQuery('INSERT INTO AI_Models SET '.implode(', ', $changes));
}
$refreshParent = true;
}
$redirect = '?view=options&tab=ai_models';
} else if ($action == 'delete') {
if ( !empty($_REQUEST['markIds']) ) {
foreach ($_REQUEST['markIds'] as $Id) {
dbQuery('DELETE FROM AI_Models WHERE Id=?', array($Id));
}
$refreshParent = true;
}
$redirect = '?view=options&tab=ai_models';
} else {
ZM\Error("Unknown action $action in saving AI Model");
}
?>
+8
View File
@@ -26,6 +26,14 @@ if ( !canEdit('System') ) {
global $error_message;
// AI object-detection CRUD lives in dedicated action files. The list views post
// here (view=options) with object=ai_* for deletes; route those to the matching
// handler, which processes both save and delete via $action.
if ( isset($_REQUEST['object']) && in_array($_REQUEST['object'], array('ai_model', 'ai_dataset', 'ai_class'), true) ) {
require_once('includes/actions/'.$_REQUEST['object'].'.php');
return;
}
if ( $action == 'delete' ) {
if ( isset($_REQUEST['object']) ) {
if ( $_REQUEST['object'] == 'server' ) {
+1 -1
View File
@@ -782,7 +782,7 @@ function MonitorStream(monitorData) {
Very, very rarely, on the MONTAGE PAGE THERE MAY BE AN ERROR OF THE TYPE: TypeError: Failed to execute 'remove' on 'SourceBuffer': The start provided (0) is outside the range (0, 0).
Possibly due to high CPU load, the browser does not have time to process or the "src" attribute was removed from the object.
*/
this.mseSourceBuffer.remove(0, Infinity);
if (this.mse.sourceBuffers.length > 0) this.mseSourceBuffer.remove(0, Infinity);
} catch (e) {
console.warn(`${dateTimeToISOLocal(new Date())} An error occurred while cleaning Source Buffer for ID=${this.id}`, e);
reject(e);
+3
View File
@@ -60,7 +60,10 @@ $SLANG = array(
'24BitColour' => '24 bit colour',
'32BitColour' => '32 bit colour',
'8BitGrey' => '8 bit greyscale',
'AddNewClass' => 'Add New Class',
'AddNewControl' => 'Add New Control',
'AddNewDataset' => 'Add New Dataset',
'AddNewModel' => 'Add New Model',
'AddNewMonitor' => 'Add',
'AddMonitorDisabled' => 'Your user is not allowed to add a new monitor',
'AddNewServer' => 'Add New Server',
+9
View File
@@ -1169,6 +1169,15 @@ function getOptionsHTML($forLeftBar = false, $customLabel = null) {
]);
$zmMenu::buildSubMenuOptions($categoryDisplayOrder);
// AI object-detection management tabs. These are not Config categories, so
// register them explicitly with readable labels instead of the auto-generated
// "Ai_datasets" style names.
if ( canView('System') ) {
$zmMenu::$submenuOptionsItems['ai_datasets'] = translate('AI Datasets');
$zmMenu::$submenuOptionsItems['ai_models'] = translate('AI Models');
$zmMenu::$submenuOptionsItems['ai_classes'] = translate('AI Classes');
}
if ( canView('System') ) {
if ($forLeftBar) {
global $view;
@@ -0,0 +1,76 @@
<form name="classesForm" method="post" action="?">
<div id="options">
<div class="row">
<input type="hidden" name="view" value="<?php echo $view ?>"/>
<input type="hidden" name="tab" value="<?php echo $tab ?>"/>
<input type="hidden" name="action" value="delete"/>
<input type="hidden" name="object" value="ai_class"/>
<div class="col">
<div id="contentButtons">
<button type="button" id="NewClassBtn" title="<?php echo translate('AddNewClass') ?>" disabled="disabled">
<i class="material-icons">add_circle</i>
<span class="text">&nbsp;<?php echo translate('AddNewClass') ?></span>
</button>
<button type="submit" class="btn-danger" name="deleteBtn" value="Delete" title="<?php echo translate('Delete') ?>" disabled="disabled">
<i class="material-icons">delete</i>
<span class="text">&nbsp;<?php echo translate('Delete') ?></span>
</button>
</div>
</div> <!-- .col -->
</div> <!-- .row -->
<div class="row mb-3">
<div class="col-md-4">
<label for="datasetFilter"><?php echo translate('FilterByDataset') ?>:</label>
<select id="datasetFilter" class="form-control">
<option value=""><?php echo translate('AllDatasets') ?></option>
<?php
$datasets_result = dbQuery('SELECT Id, Name FROM AI_Datasets ORDER BY Name');
if ($datasets_result) {
while ($dataset_row = dbFetchNext($datasets_result)) {
echo '<option value="'.$dataset_row['Id'].'">'.validHtmlStr($dataset_row['Name']).'</option>';
}
}
?>
</select>
</div>
</div>
<div class="wrapper-scroll-table">
<div class="row">
<div class="col">
<table id="contentTable" class="table table-striped">
<thead class="thead-highlight">
<tr>
<th class="colMark"><?php echo translate('Mark') ?></th>
<th data-sortable="true" class="colId"><?php echo translate('Id') ?></th>
<th data-sortable="true" class="colDataset"><?php echo translate('Dataset') ?></th>
<th data-sortable="true" class="colClassName"><?php echo translate('ClassName') ?></th>
<th data-sortable="true" class="colClassIndex"><?php echo translate('ClassIndex') ?></th>
<th data-sortable="true" class="colDescription"><?php echo translate('Description') ?></th>
</tr>
</thead>
<tbody>
<?php
$result = dbQuery('SELECT c.*, d.Name as DatasetName FROM AI_Object_Classes c LEFT JOIN AI_Datasets d ON c.DatasetId = d.Id ORDER BY d.Name, c.ClassIndex');
if ($result) {
while ($row = dbFetchNext($result)) {
$class_opt = 'class="classCol" data-cid="'.$row['Id'].'" data-dataset-id="'.$row['DatasetId'].'"';
?>
<tr>
<td class="colMark"><input type="checkbox" name="markIds[]" value="<?php echo $row['Id'] ?>" data-on-click-this="configureDeleteButton"<?php if ( !$canEdit ) { ?> disabled="disabled"<?php } ?>/></td>
<td class="colId"><?php echo makeLink('#', validHtmlStr($row['Id']), $canEdit, $class_opt) ?></td>
<td class="colDataset"><?php echo makeLink('#', validHtmlStr($row['DatasetName']), $canEdit, $class_opt) ?></td>
<td class="colClassName"><?php echo makeLink('#', validHtmlStr($row['ClassName']), $canEdit, $class_opt) ?></td>
<td class="colClassIndex"><?php echo makeLink('#', validHtmlStr($row['ClassIndex']), $canEdit, $class_opt) ?></td>
<td class="colDescription"><?php echo makeLink('#', validHtmlStr($row['Description']), $canEdit, $class_opt) ?></td>
</tr>
<?php
}
}
?>
</tbody>
</table>
</div> <!-- .col -->
</div> <!-- .row -->
</div> <!-- .wrapper-scroll-table -->
</div> <!-- .options -->
</form>
@@ -0,0 +1,60 @@
<form name="datasetsForm" method="post" action="?">
<div id="options">
<div class="row">
<input type="hidden" name="view" value="<?php echo $view ?>"/>
<input type="hidden" name="tab" value="<?php echo $tab ?>"/>
<input type="hidden" name="action" value="delete"/>
<input type="hidden" name="object" value="ai_dataset"/>
<div class="col">
<div id="contentButtons">
<button type="button" id="NewDatasetBtn" title="<?php echo translate('AddNewDataset') ?>" disabled="disabled">
<i class="material-icons">add_circle</i>
<span class="text">&nbsp;<?php echo translate('AddNewDataset') ?></span>
</button>
<button type="submit" class="btn-danger" name="deleteBtn" value="Delete" title="<?php echo translate('Delete') ?>" disabled="disabled">
<i class="material-icons">delete</i>
<span class="text">&nbsp;<?php echo translate('Delete') ?></span>
</button>
</div>
</div> <!-- .col -->
</div> <!-- .row -->
<div class="wrapper-scroll-table">
<div class="row">
<div class="col">
<table id="contentTable" class="table table-striped">
<thead class="thead-highlight">
<tr>
<th class="colMark"><?php echo translate('Mark') ?></th>
<th data-sortable="true" class="colId"><?php echo translate('Id') ?></th>
<th data-sortable="true" class="colName"><?php echo translate('Name') ?></th>
<th data-sortable="true" class="colVersion"><?php echo translate('Version') ?></th>
<th data-sortable="true" class="colNumClasses"><?php echo translate('NumClasses') ?></th>
<th data-sortable="true" class="colDescription"><?php echo translate('Description') ?></th>
</tr>
</thead>
<tbody>
<?php
$result = dbQuery('SELECT * FROM AI_Datasets ORDER BY Name');
if ($result) {
while ($row = dbFetchNext($result)) {
$dataset_opt = 'class="datasetCol" data-did="'.$row['Id'].'"';
?>
<tr>
<td class="colMark"><input type="checkbox" name="markIds[]" value="<?php echo $row['Id'] ?>" data-on-click-this="configureDeleteButton"<?php if ( !$canEdit ) { ?> disabled="disabled"<?php } ?>/></td>
<td class="colId"><?php echo makeLink('#', validHtmlStr($row['Id']), $canEdit, $dataset_opt) ?></td>
<td class="colName"><?php echo makeLink('#', validHtmlStr($row['Name']), $canEdit, $dataset_opt) ?></td>
<td class="colVersion"><?php echo makeLink('#', validHtmlStr($row['Version']), $canEdit, $dataset_opt) ?></td>
<td class="colNumClasses"><?php echo makeLink('#', validHtmlStr($row['NumClasses']), $canEdit, $dataset_opt) ?></td>
<td class="colDescription"><?php echo makeLink('#', validHtmlStr($row['Description']), $canEdit, $dataset_opt) ?></td>
</tr>
<?php
}
}
?>
</tbody>
</table>
</div> <!-- .col -->
</div> <!-- .row -->
</div> <!-- .wrapper-scroll-table -->
</div> <!-- .options -->
</form>
@@ -0,0 +1,66 @@
<form name="modelsForm" method="post" action="?">
<div id="options">
<div class="row">
<input type="hidden" name="view" value="<?php echo $view ?>"/>
<input type="hidden" name="tab" value="<?php echo $tab ?>"/>
<input type="hidden" name="action" value="delete"/>
<input type="hidden" name="object" value="ai_model"/>
<div class="col">
<div id="contentButtons">
<button type="button" id="NewModelBtn" title="<?php echo translate('AddNewModel') ?>" disabled="disabled">
<i class="material-icons">add_circle</i>
<span class="text">&nbsp;<?php echo translate('AddNewModel') ?></span>
</button>
<button type="submit" class="btn-danger" name="deleteBtn" value="Delete" title="<?php echo translate('Delete') ?>" disabled="disabled">
<i class="material-icons">delete</i>
<span class="text">&nbsp;<?php echo translate('Delete') ?></span>
</button>
</div>
</div> <!-- .col -->
</div> <!-- .row -->
<div class="wrapper-scroll-table">
<div class="row">
<div class="col">
<table id="contentTable" class="table table-striped">
<thead class="thead-highlight">
<tr>
<th class="colMark"><?php echo translate('Mark') ?></th>
<th data-sortable="true" class="colId"><?php echo translate('Id') ?></th>
<th data-sortable="true" class="colName"><?php echo translate('Name') ?></th>
<th data-sortable="true" class="colFramework"><?php echo translate('Framework') ?></th>
<th data-sortable="true" class="colVersion"><?php echo translate('Version') ?></th>
<th data-sortable="true" class="colDataset"><?php echo translate('Dataset') ?></th>
<th data-sortable="true" class="colModelPath"><?php echo translate('ModelPath') ?></th>
<th data-sortable="true" class="colEnabled"><?php echo translate('Enabled') ?></th>
<th data-sortable="true" class="colDescription"><?php echo translate('Description') ?></th>
</tr>
</thead>
<tbody>
<?php
$result = dbQuery('SELECT m.*, d.Name as DatasetName FROM AI_Models m LEFT JOIN AI_Datasets d ON m.DatasetId = d.Id ORDER BY m.Name');
if ($result) {
while ($row = dbFetchNext($result)) {
$model_opt = 'class="modelCol" data-mid="'.$row['Id'].'"';
?>
<tr>
<td class="colMark"><input type="checkbox" name="markIds[]" value="<?php echo $row['Id'] ?>" data-on-click-this="configureDeleteButton"<?php if ( !$canEdit ) { ?> disabled="disabled"<?php } ?>/></td>
<td class="colId"><?php echo makeLink('#', validHtmlStr($row['Id']), $canEdit, $model_opt) ?></td>
<td class="colName"><?php echo makeLink('#', validHtmlStr($row['Name']), $canEdit, $model_opt) ?></td>
<td class="colFramework"><?php echo makeLink('#', validHtmlStr($row['Framework']), $canEdit, $model_opt) ?></td>
<td class="colVersion"><?php echo makeLink('#', validHtmlStr($row['Version']), $canEdit, $model_opt) ?></td>
<td class="colDataset"><?php echo makeLink('#', validHtmlStr($row['DatasetName']), $canEdit, $model_opt) ?></td>
<td class="colModelPath"><?php echo makeLink('#', validHtmlStr($row['ModelPath']), $canEdit, $model_opt) ?></td>
<td class="colEnabled"><?php echo makeLink('#', $row['Enabled'] ? translate('Yes') : translate('No'), $canEdit, $model_opt) ?></td>
<td class="colDescription"><?php echo makeLink('#', validHtmlStr($row['Description']), $canEdit, $model_opt) ?></td>
</tr>
<?php
}
}
?>
</tbody>
</table>
</div> <!-- .col -->
</div> <!-- .row -->
</div> <!-- .wrapper-scroll-table -->
</div> <!-- .options -->
</form>
+7 -1
View File
@@ -192,9 +192,15 @@ function initPage() {
});
document.querySelectorAll('select[name="newMonitor[Type]"]').forEach(function(el) {
el.onchange = function() {
// Reload the form to render the type-specific fields WITHOUT saving. The
// monitor view repopulates from the posted newMonitor values, so the
// selected Type and anything already entered are preserved. Saving here
// would fail for a brand-new monitor whose Width/Height are not set yet
// (both are NOT NULL), leaving the type change unpersisted. A plain
// submit posts no action=save, matching the Method field's submitTab.
const form = document.getElementById('contentForm');
form.tab.value = 'general';
saveMonitorDataPrepare(document.getElementById('contentForm'), false, 'reload');
form.submit();
};
});
document.querySelectorAll('input[name="newMonitor[ImageBufferCount]"],input[name="newMonitor[MaxImageBufferCount]"],input[name="newMonitor[Width]"],input[name="newMonitor[Height]"],input[name="newMonitor[PreEventCount]"]').forEach(function(el) {
+94
View File
@@ -54,6 +54,72 @@ function AddNewRole(el) {
window.location.assign(url);
}
// Load the Dataset Modal HTML via Ajax call
function getDatasetModal(did) {
$j.getJSON(thisUrl + '?request=modal&modal=ai_dataset&id=' + did)
.done(function(data) {
insertModalHtml('DatasetModal', data.html);
$j('#DatasetModal').modal('show');
})
.fail(logAjaxFail);
}
function enableDatasetModal() {
$j(".datasetCol").click(function(evt) {
evt.preventDefault();
const did = $j(this).data('did');
getDatasetModal(did);
});
$j('#NewDatasetBtn').click(function(evt) {
evt.preventDefault();
getDatasetModal(0);
});
}
// Load the Model Modal HTML via Ajax call
function getModelModal(mid) {
$j.getJSON(thisUrl + '?request=modal&modal=ai_model&id=' + mid)
.done(function(data) {
insertModalHtml('ModelModal', data.html);
$j('#ModelModal').modal('show');
})
.fail(logAjaxFail);
}
function enableModelModal() {
$j(".modelCol").click(function(evt) {
evt.preventDefault();
const mid = $j(this).data('mid');
getModelModal(mid);
});
$j('#NewModelBtn').click(function(evt) {
evt.preventDefault();
getModelModal(0);
});
}
// Load the Class Modal HTML via Ajax call
function getClassModal(cid) {
$j.getJSON(thisUrl + '?request=modal&modal=ai_class&id=' + cid)
.done(function(data) {
insertModalHtml('ClassModal', data.html);
$j('#ClassModal').modal('show');
})
.fail(logAjaxFail);
}
function enableClassModal() {
$j(".classCol").click(function(evt) {
evt.preventDefault();
const cid = $j(this).data('cid');
getClassModal(cid);
});
$j('#NewClassBtn').click(function(evt) {
evt.preventDefault();
getClassModal(0);
});
}
let newMenuItemIndex = 0;
function addMenuItem() {
@@ -144,12 +210,40 @@ function DeleteRole() {
function initPage() {
const NewStorageBtn = $j('#NewStorageBtn');
const NewServerBtn = $j('#NewServerBtn');
const NewDatasetBtn = $j('#NewDatasetBtn');
const NewModelBtn = $j('#NewModelBtn');
const NewClassBtn = $j('#NewClassBtn');
if ( canEdit.System ) enableStorageModal();
if ( canEdit.System ) enableServerModal();
if ( canEdit.System ) enableDatasetModal();
if ( canEdit.System ) enableModelModal();
if ( canEdit.System ) enableClassModal();
NewStorageBtn.prop('disabled', !canEdit.System);
NewServerBtn.prop('disabled', !canEdit.System);
NewDatasetBtn.prop('disabled', !canEdit.System);
NewModelBtn.prop('disabled', !canEdit.System);
NewClassBtn.prop('disabled', !canEdit.System);
// Dataset filter functionality for AI Classes tab
$j('#datasetFilter').change(function() {
var datasetId = $j(this).val();
var $rows = $j('#contentTable tbody tr');
if (datasetId === '') {
$rows.show();
} else {
$rows.each(function() {
var $row = $j(this);
var rowDatasetId = $row.find('.classCol').data('dataset-id');
if (rowDatasetId == datasetId) {
$row.show();
} else {
$row.hide();
}
});
}
});
$j('.bootstraptable').bootstrapTable({icons: icons}).show();
+8 -2
View File
@@ -143,8 +143,14 @@ foreach (array_map('basename', glob('skins/'.$skin.'/css/*', GLOB_ONLYDIR)) as $
include('_options_menu.php');
} else if ($tab == 'encoderTemplates') {
include('_options_encoderTemplates.php');
} // $tab == API/menu/encoderTemplates
else {
} else if ($tab == 'ai_datasets') {
include('_options_ai_datasets.php');
} else if ($tab == 'ai_models') {
include('_options_ai_models.php');
} else if ($tab == 'ai_classes') {
include('_options_ai_classes.php');
} // $tab == API/menu/encoderTemplates/ai_*
else {
$config = array();
$configCats = array();