mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-07 09:52:03 -04:00
Merge branch 'master' into patch-865636
This commit is contained in:
25 files changed
+1406
-97
No files matched your search
@@ -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;
|
||||
@@ -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');
|
||||
@@ -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
|
||||
--
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
1.39.16
|
||||
1.39.17
|
||||
@@ -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">×</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>
|
||||
@@ -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">×</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>
|
||||
@@ -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">×</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
@@ -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
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
?>
|
||||
@@ -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");
|
||||
}
|
||||
?>
|
||||
@@ -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");
|
||||
}
|
||||
?>
|
||||
@@ -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' ) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"> <?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"> <?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"> <?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"> <?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"> <?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"> <?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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user