diff --git a/.gitignore b/.gitignore index 4d0415c..9e6cb32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.asv +*.log Data/RES101TrainedNET.mat FSM.slx FSM_grt_rtw/* diff --git a/AutoOpticalInspection.m b/AutoOpticalInspection.m new file mode 100644 index 0000000..3b43a43 --- /dev/null +++ b/AutoOpticalInspection.m @@ -0,0 +1,140 @@ +classdef AutoOpticalInspection < handle + %AOI Automated Optical Inspection + + properties + % Logger Instance + log + % Finite State Machine + fsm + % Timer to handle stepping the FSM + tmr + % Modules + mod_im_proc + mod_im_acq + mod_proc_ctrl + end + + methods + function self = AutoOpticalInspection() + %AOI Construct an instance of this class + + % Add the modules path + addpath('./Modules'); + + % Set up logging + import logging.logging.*; + logPath = './Logs'; + logName = [datestr(datetime('now'),'yyyy_mm_dd__HH_MM_SS') '.log']; + self.log = logging.getLogger('AOI_Logger','path',[logPath '/' logName]); + + % Set up Finite State Machine + self.fsm = StateFlowChart(); + + + % === Instantiate Modules === + % Image Processing + self.mod_im_proc = Mod_ImageProcessor(self.log); + + % Image Acquisition + self.mod_im_acq = Mod_ImageAcquisition(self.log); + + % Process Control Master with Event Trigger Methods + methodSignatures = struct(); + methodSignatures.method_begin_inspection = @self.ev_Req_Begin_Inspection; + methodSignatures.method_complete_inspection = @self.ev_Req_Complete_Inspection; + methodSignatures.method_capture_image = @self.ev_Req_Image_Capture; + methodSignatures.method_setState_CurrentPose = @self.setState_CurrentPose; + self.mod_proc_ctrl = Mod_ProcessControl(self.log,methodSignatures); + + % Provide the FSM with method handles for relavant tasks from + % modules + self.fsm.functions.Send_Section_Statuses = @self.func_send_section_statuses; + self.fsm.functions.Send_Capture_Complete = @self.func_send_capture_complete; + self.fsm.functions.Capture_Image = @self.func_captureImage; + self.fsm.functions.Process_Images = @self.func_processImages; + + % === Set up timer to Execute FSM === + self.tmr = timer('ExecutionMode','fixedSpacing'); + self.tmr.TimerFcn = {@self.stepFSM_Callback}; + + % === Begin the Process === + self.mod_proc_ctrl.connect(); + self.tmr.start; + end + + function self = setState_CurrentPose(self, curPoseData) + %SETSTATE_CURRENTPOSE Passes current pose information to FSM + % CurPoseID should be a member of the curPoseData struct, it is + % pulled out and specified to the FSM, as it is a required + % value for image acquisition. + + % Generic Data Container for Current Pose Information + self.fsm.curPoseData = curPoseData; + % Set the current PoseID for the FSM + self.fsm.curPoseID = curPoseData.PoseID; + end + + function self = stepFSM_Callback(self,obj,event) + self.fsm.step; + end + + function self = dispose(self) + self.log.info('AutoOpticalInspection shutting down.'); + % Stop and Delete Timer + stop(self.tmr); + delete(self.tmr); + + % Close Connection to the Process Controller + self.mod_proc_ctrl.dispose(); + + % Close Connection to the Camera + self.mod_im_acq.dispose(); + + % Delete Instances of Modules + delete(self.mod_proc_ctrl); + delete(self.mod_im_acq); + delete(self.mod_im_proc); + + % Delete FSM + delete(self.fsm); + end + end + + + % Module Functions to be called from FSM + methods + function func_send_section_statuses(self, statusData) + self.mod_proc_ctrl.send_section_statuses(statusData); + end + + function func_send_capture_complete(self) + self.mod_proc_ctrl.send_capture_complete(); + end + + function img = func_captureImage(self,curPoseData) + img = self.mod_im_acq.captureImage(curPoseData); + end + + function statusData = func_processImages(self,imageContainer) + statusData = self.mod_im_proc.processImages(imageContainer); + end + end + + + % Event Trigger Methods + methods + function self = ev_Req_Begin_Inspection(self) + self.fsm.ev_Req_Begin_Inspection(); + end + + function self = ev_Req_Complete_Inspection(self) + self.fsm.inspection_ready = true; + end + + function self = ev_Req_Image_Capture(self) + self.fsm.capture_ready = true; + end + end + +end + diff --git a/Modules/+logging/clearLogger.m b/Modules/+logging/clearLogger.m new file mode 100644 index 0000000..77c884c --- /dev/null +++ b/Modules/+logging/clearLogger.m @@ -0,0 +1,5 @@ +function clearLogger(name) + [~, destructor] = logging.getLogger(name); + destructor(); +end + diff --git a/Modules/+logging/getLogger.m b/Modules/+logging/getLogger.m new file mode 100644 index 0000000..eef4d3c --- /dev/null +++ b/Modules/+logging/getLogger.m @@ -0,0 +1,28 @@ +function [obj, deleteLogger] = getLogger(name, varargin) + persistent loggers; + logger_found = false; + if ~isempty(loggers) + for logger = loggers + if strcmp(logger.name, name) + obj = logger; + logger_found = true; + break; + end + end + end + if ~logger_found + obj = logging.logging(name, varargin{:}); + loggers = [loggers, obj]; + end + + deleteLogger = @() deleteLogInstance(); + + function deleteLogInstance() + if ~logger_found + error(['logger for file [ ' name ' ] not found']) + end + loggers = loggers(loggers ~= obj); + delete(obj); + clear('obj'); + end +end diff --git a/Modules/+logging/logging.m b/Modules/+logging/logging.m new file mode 100644 index 0000000..cdea6d9 --- /dev/null +++ b/Modules/+logging/logging.m @@ -0,0 +1,262 @@ +classdef logging < handle + %LOGGING Simple logging framework. + % + % Author: + % Dominique Orban + % Heavily modified version of 'log4m': http://goo.gl/qDUcvZ + % + + properties (Constant) + ALL = int8(0); + TRACE = int8(1); + DEBUG = int8(2); + INFO = int8(3); + WARNING = int8(4); + ERROR = int8(5); + CRITICAL = int8(6); + OFF = int8(7); + + colors_terminal = containers.Map(... + {'normal', 'red', 'green', 'yellow', 'blue', 'brightred'}, ... + {'%s', '\033[31m%s\033[0m', '\033[32m%s\033[0m', '\033[33m%s\033[0m', ... + '\033[34m%s\033[0m', '\033[1;31m%s\033[0m'}); + + level_colors = containers.Map(... + {logging.logging.INFO, logging.logging.ERROR, logging.logging.TRACE, ... + logging.logging.WARNING, logging.logging.DEBUG, logging.logging.CRITICAL}, ... + {'normal', 'red', 'green', 'yellow', 'blue', 'brightred'}); + + levels = containers.Map(... + {logging.logging.ALL, logging.logging.TRACE, logging.logging.DEBUG, ... + logging.logging.INFO, logging.logging.WARNING, logging.logging.ERROR, ... + logging.logging.CRITICAL, logging.logging.OFF}, ... + {'ALL', 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'OFF'}); + end + + properties (SetAccess=immutable) + level_numbers; + level_range; + end + + properties (SetAccess=protected) + name; + fullpath = 'logging.log'; % Default log file + logfmt = '%-s %-23s %-8s %s\n'; + logfid = -1; + logcolors = logging.logging.colors_terminal; + using_terminal; + end + + properties (Hidden,SetAccess=protected) + datefmt_ = 'yyyy-mm-dd HH:MM:SS,FFF'; + logLevel_ = logging.logging.INFO; + commandWindowLevel_ = logging.logging.INFO; + end + + properties (Dependent) + datefmt; + logLevel; + commandWindowLevel; + end + + methods(Static) + function [name, line] = getCallerInfo(self) + + if nargin > 0 && self.ignoreLogging() + name = []; + line = []; + return + end + [ST, ~] = dbstack(); + offset = min(size(ST, 1), 3); + name = ST(offset).name; + line = ST(offset).line; + end + end + + methods + + function setFilename(self, logPath) + [self.logfid, message] = fopen(logPath, 'a'); + + if self.logfid < 0 + warning(['Problem with supplied logfile path: ' message]); + self.logLevel_ = logging.logging.OFF; + end + + self.fullpath = logPath; + end + + function setCommandWindowLevel(self, level) + self.commandWindowLevel = level; + end + + function setLogLevel(self, level) + self.logLevel = level; + end + + function tf = ignoreLogging(self) + tf = self.commandWindowLevel_ == self.OFF && self.logLevel_ == self.OFF; + end + + function trace(self, varargin) + [caller_name, ~] = self.getCallerInfo(self); + self.writeLog(self.TRACE, caller_name, varargin{:}); + end + + function debug(self, varargin) + [caller_name, ~] = self.getCallerInfo(self); + self.writeLog(self.DEBUG, caller_name, varargin{:}); + end + + function info(self, varargin) + [caller_name, ~] = self.getCallerInfo(self); + self.writeLog(self.INFO, caller_name, varargin{:}); + end + + function warn(self, varargin) + [caller_name, ~] = self.getCallerInfo(self); + self.writeLog(self.WARNING, caller_name, varargin{:}); + end + + function error(self, varargin) + [caller_name, ~] = self.getCallerInfo(self); + self.writeLog(self.ERROR, caller_name, varargin{:}); + end + + function critical(self, varargin) + [caller_name, ~] = self.getCallerInfo(self); + self.writeLog(self.CRITICAL, caller_name, varargin{:}); + end + + function self = logging(name, varargin) + levelkeys = self.levels.keys; + self.level_numbers = containers.Map(... + self.levels.values, levelkeys); + levelkeys = cell2mat(self.levels.keys); + self.level_range = [min(levelkeys), max(levelkeys)]; + + p = inputParser(); + p.addRequired('name', @ischar); + p.addParameter('path', '', @ischar); + p.addParameter('logLevel', self.logLevel); + p.addParameter('commandWindowLevel', self.commandWindowLevel); + p.addParameter('datefmt', self.datefmt_); + p.parse(name, varargin{:}); + r = p.Results; + + self.name = r.name; + self.commandWindowLevel = r.commandWindowLevel; + self.datefmt = r.datefmt; + if ~isempty(r.path) + self.setFilename(r.path); % Opens the log file. + self.logLevel = r.logLevel; + else + self.logLevel_ = logging.logging.OFF; + end + % Use terminal logging if swing is disabled in matlab environment. + swingError = javachk('swing'); + self.using_terminal = (~ isempty(swingError) && strcmp(swingError.identifier, 'MATLAB:javachk:thisFeatureNotAvailable')) || ~desktop('-inuse'); + end + + function delete(self) + if self.logfid > -1 + fclose(self.logfid); + end + end + + function writeLog(self, level, caller, message, varargin) + + level = self.getLevelNumber(level); + if self.commandWindowLevel_ <= level || self.logLevel_ <= level + timestamp = datestr(now, self.datefmt_); + levelStr = logging.logging.levels(level); + logline = sprintf(self.logfmt, caller, timestamp, levelStr, self.getMessage(message, varargin{:})); + end + + if self.commandWindowLevel_ <= level + if self.using_terminal + level_color = self.level_colors(level); + else + level_color = self.level_colors(logging.logging.INFO); + end + fprintf(self.logcolors(level_color), logline); + end + + if self.logLevel_ <= level && self.logfid > -1 + fprintf(self.logfid, '%s', logline); + end + end + + function set.datefmt(self, fmt) + try + datestr(now(), fmt); + catch + error('Invalid date format'); + end + self.datefmt_ = fmt; + end + + function fmt = get.datefmt(self) + fmt = self.datefmt_; + end + + function set.logLevel(self, level) + level = self.getLevelNumber(level); + if level > logging.logging.OFF || level < logging.logging.ALL + error('invalid logging level'); + end + self.logLevel_ = level; + end + + function level = get.logLevel(self) + level = self.logLevel_; + end + + function set.commandWindowLevel(self, level) + self.commandWindowLevel_ = self.getLevelNumber(level); + end + + function level = get.commandWindowLevel(self) + level = self.commandWindowLevel_; + end + + + end + + methods (Hidden) + function level = getLevelNumber(self, level) + % LEVEL = GETLEVELNUMBER(LEVEL) + % + % Converts charecter-based level names to level numbers + % used internally by logging. + % + % If given a number, it makes sure the number is valid + % then returns it unchanged. + % + % This allows users to specify levels by name or number. + if isinteger(level) && self.level_range(1) <= level && level <= self.level_range(2) + return + else + level = self.level_numbers(level); + end + end + + function message = getMessage(~, message, varargin) + + if isa(message, 'function_handle') + message = message(); + end + + if nargin > 2 + message = sprintf(message, varargin{:}); + end + + [rows, ~] = size(message); + if rows > 1 + message = sprintf('\n %s', evalc('disp(message)')); + end + end + + end +end diff --git a/Modules/+logging/logging4matlab/LICENSE b/Modules/+logging/logging4matlab/LICENSE new file mode 100644 index 0000000..efb2aa4 --- /dev/null +++ b/Modules/+logging/logging4matlab/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 optimizers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Modules/+logging/logging4matlab/README.md b/Modules/+logging/logging4matlab/README.md new file mode 100644 index 0000000..fe06899 --- /dev/null +++ b/Modules/+logging/logging4matlab/README.md @@ -0,0 +1,165 @@ +# Logging for Matlab + +This simple logging module is a modification of [`log4m`](http://goo.gl/qDUcvZ) +with the following improvements: + +* multiple loggers can be created and retrieved by name (à la Python) +* different logging levels appear in different colors if using Matlab in the terminal + +Each logger's output can be directed to the standard output and/or to a file. + +Each logger is assigned a logging level that will control the amount of output. +The possible levels are, from high to low: + +* ALL (highest) +* TRACE +* DEBUG +* INFO (default) +* WARNING +* ERROR +* CRITICAL +* OFF (lowest) + +The default level in INFO. +If a logger outputs at a level lower than or equal to its assigned level, the output will be logged. +To silence a logger, set its level to OFF. + +All loggers output a string according to the Matlab format `'%-s %-23s %-8s %s\n'`. +Note that a newline is always appended, so there is no need to terminate log lines +with a newline. +The format is as follows: +* `%-s` is used to display the caller name, i.e., the function or method in which + logging occured +* `%-23s` is used for a time stamp of the form `2016-09-14 14:23:44,271` +* `%-8s` is used for the logging level +* `%s` is used for the message to be logged. + +## API + +An instance of the `logging` class is created using the `logging.getLogger` function. +The first argument for this function must be the name of the logger. Four additional +optional arguments are also available. These can either be provided as name/value +pairs (such as `logger.getlogger(name, 'path', path)`) or as a struct where the +field names are the names of the argument (such as `logger.getlogger(name, struct('path', path)`). +The available arguments are: + +* `path`: The path to the log file. If this is not specified or is an empty string, + then logging to a file is disabled. + This must be a string. +* `logLevel`: set the file log level. + Only log entries with a level greater than or equal to this level will be saved. + This can either be a string or an integer. + Note that this argument will be ignored if `path` is empty or not specified. +* `commandWindowLevel`: set the command window log level. + Only log entries with a level greater than or equal to this level will be displayed. + This can either be a string or an integer. +* `datefmt`: the date/time format string. + This contains the date/time format string used by the logs. + The format must be compatible with the built-in `datestr` function. + This must be a string. + +If `logger` is an instance of the `logging` class, the following methods can be used +to log output at different levels: + +* `logger.trace(string)`: output `string` at level TRACE. + This level is mostly used to trace a code path. +* `logger.debug(string)`: output `string` at level DEBUG. + This level is mostly used to log debugging output that may help identify an issue + or verify correctness by inspection. +* `logger.info(string)`: output `string` at level INFO. + This level is intended for general user messages about the progress of the program. +* `logger.warn(string)`: output `string` at level WARNING (unrelated to Matlab's `warning()` function). + This level is used to alert the user of a possible problem. +* `logger.error(string)`: output `string` at level ERROR (unrelated to Matlab's `error()` function). + This level is used for non-critical errors that can endanger correctness. +* `logger.critical(string)`: output `string` at level CRITICAL + This level is used for critical errors that definitely endanger correctness. + +The following utility methods are also available: + +* `logger.setFilename(string)`: set the log file to `string`. + This can be used to specify or change the file logs are saved to. +* `logger.setCommandWindowLevel(level)`: set the command window log level to `level`. + Only log entries with a level greater than or equal to `level` will be displayed. + `level` can either be a string or an integer. +* `logger.setLogLevel(level)`: set the file log level to `level`. + Only log entries with a level greater than or equal to `level` will be saved. + `level` can either be a string or an integer. + Note that even if the level is changed, nothing will be written if a valid + filename has not been set for the log. + +The following properties can be read or written: + +* `logger.datefmt`: the date/time format string. + This contains the date/time format string used by the logs. + The format must be compatible with the built-in `datestr` function. +* `logger.commandWindowLevel`: the command window log level. + Only log entries with a level greater than or equal to `level` will be displayed. + It can be set with either a string or an integer, but will always return an integer. +* `logger.logLevel`: the file log level. + Only log entries with a level greater than or equal to `level` will be saved. + It can be set with either a string or an integer, but will always return an integer. + +The following properties are read-only (note that these are called in a different way): + +* `logging.logging.ALL`: The integer value for the `ALL` level (0). +* `logging.logging.TRACE`: The integer value for the `TRACE` level (1). +* `logging.logging.DEBUG`: The integer value for the `DEBUG` level (2). +* `logging.logging.INFO`: The integer value for the `INFO` level (3). +* `logging.logging.WARNING`: The integer value for the `WARNING` level (4). +* `logging.logging.ERROR`: The integer value for the `ERROR` level (5). +* `logging.logging.CRITICAL`: The integer value for the `CRITICAL` level (6). +* `logging.logging.OFF`: The integer value for the `OFF` level (6). + Note that there is no corresponding write method for this level, + so if this level is set no logging will take place. + +## Examples + +A logger at default level INFO logs messages at levels INFO, WARNING, ERROR and CRITICAL, but not at levels TRACE or DEBUG: + +```matlab +>> addpath('/path/to/logging4matlab') +>> logger = logging.getLogger('mylogger') % new logger with default level INFO +>> logger.info('life is just peachy') +logging.info 2016-09-14 15:10:06,049 INFO life is just peachy +>> logger.debug('Easy as pi! (Euclid)') % produces no output +>> logger.critical('run away!') +logging.critical 2016-09-14 15:12:37,652 CRITICAL run away! +``` + +Use formatting for logged messages similar to `sprintf` or `fprintf` + +```matlab +>> logger.critical('Item %d (%s) not found', 217, 'foo'); +logging.critical 2016-09-14 15:12:37,652 CRITICAL Item 217 (foo) not found +``` + +A logger's assigned level for the command window (or terminal) can be changed: + +```matlab +>> logger.setCommandWindowLevel(logging.logging.WARNING) +``` + +A logger can also output to file: + +```matlab +>> logger2 = logging.getLogger('myotherlogger', 'path', '/tmp/logger2.log') +>> logger.setLogLevel(logging.logging.WARNING) +``` + +Output to either the command window or a file can be suppressed with `logging.logging.OFF`. + +# FAQ + +1. *Why is there no colored logging in the Matlab command window?* + I haven't gotten around to evaluating the performance of [`cprintf`](https://goo.gl/Nw5OOy), + which seems to be the only viable option for colored output in the command window. + Pull request welcome! +2. *Can I change the colors?* + Currently, no, but feel free to submit a pull request! +3. *Can I change the format string used by loggers?* + Currently, no, but feel free to submit a pull request! + +# Tests + +Invoke `runtests('test')` in a MATLAB prompt to run the unit tests. diff --git a/Modules/+logging/logging4matlab/test/loggingTest.m b/Modules/+logging/logging4matlab/test/loggingTest.m new file mode 100644 index 0000000..0291601 --- /dev/null +++ b/Modules/+logging/logging4matlab/test/loggingTest.m @@ -0,0 +1,79 @@ +classdef (SharedTestFixtures={ ... + matlab.unittest.fixtures.PathFixture('..'),... + matlab.unittest.fixtures.WorkingFolderFixture}) ... + loggingTest < matlab.unittest.TestCase + %LOGGINGTEST unit tests for the logging class + % Adding parent folder to path + % Changing working directory to a temporary folder since we may + % create files + + properties + l; % instance of logging.logging + logger_name = 'testVariableMessages'; + logging_methods = {@trace, @debug, @info, @warn, @error, @critical} + end + + methods(TestMethodSetup) + function createFigure(testCase) + testCase.l = logging.getLogger(testCase.logger_name); + end + end + + methods(TestMethodTeardown) + function closeFigure(testCase) + % loggers can be persistent. Delete logger so that tests are + % independent + logging.clearLogger(testCase.logger_name); + end + end + + methods (Test) + + function testGetMessageWithVariableNumberOfInputs(testCase) + testCase.verifyEqual(testCase.l.getMessage('Hello'), 'Hello'); + + testCase.verifyEqual(testCase.l.getMessage('Hello %s', 'world'),... + 'Hello world'); + + testCase.verifyEqual(testCase.l.getMessage('Hello %s %d', 'world', 2),... + 'Hello world 2'); + end + + function testLoggingWithVariableNumberOfInputs(testCase) + logfile_name = [testCase.logger_name '.log']; + + testCase.l.setFilename(logfile_name); + testCase.l.setLogLevel(logging.logging.CRITICAL); + + for i=1:length(testCase.logging_methods) + method_to_test = testCase.logging_methods{i}; + method_to_test(testCase.l, 'Hello'); + method_to_test(testCase.l, 'Hello %s', 'world'); + method_to_test(testCase.l, '%d', 2); + end + + % For each of the logged lines, only retrive the logged message + loggedStrings = loggingTest.getLogMessagesFromFile(logfile_name,... + '^.* CRITICAL (?.*)$'); + + testCase.verifyEqual(loggedStrings, {... + 'Hello', 'Hello world', '2'}); + + end + + end + + methods(Static = true) + function ret = getLogMessagesFromFile(fileName, token) + lines = strsplit(fileread(fileName), '\n'); + + % Last line is a empty new line. + ret = regexp(lines(1:end-1), token, 'names'); + + % Only the ``t0'' token is required. + ret = cellfun(@(p)p.t0, ret, 'UniformOutput', false); + end + end + +end + diff --git a/Modules/+logging/testspeed.m b/Modules/+logging/testspeed.m new file mode 100644 index 0000000..2234f32 --- /dev/null +++ b/Modules/+logging/testspeed.m @@ -0,0 +1,41 @@ +function testspeed(logPath) + + opts.path = logPath; + L = logging.getLogger('testlogger', opts); + + L.setCommandWindowLevel(L.TRACE); + L.setLogLevel(L.OFF); + tic; + for i=1:1e3 + L.trace('test'); + end + disp('1e3 logs when logging only to command window'); + toc; + + L.setCommandWindowLevel(L.OFF); + L.setLogLevel(L.OFF); + tic; + for i=1:1e3 + L.trace('test'); + end + disp('1e3 logs when logging is off'); + toc; + + L.setCommandWindowLevel(L.OFF); + L.setLogLevel(L.TRACE); + tic; + for i=1:1e3 + L.trace('test'); + end + disp('1e3 logs when logging to file'); + toc; + L.setCommandWindowLevel(L.OFF); + L.setLogLevel(L.TRACE); + + tic; + for i=1:1e3 + L.trace(@() 'test'); + end + disp('1e3 logs when logging to file using function handle'); + toc; +end diff --git a/Modules/Func_Capture_Image.m b/Modules/Func_Capture_Image.m deleted file mode 100644 index e7ba82d..0000000 --- a/Modules/Func_Capture_Image.m +++ /dev/null @@ -1,30 +0,0 @@ -function img = Func_Capture_Image() -%FUNC_CAPTURE_IMAGE Implementation of image capture from the camera. -% This function should return an n x m x 3 uint8 image array - - GIGE_CAM_IP_ADDRESS = '169.254.90.219'; - - % Set to 0 when actual camera is connected - test = 1; - - if(~test) - % Initialize a connection to the camera - g = gigecam(GIGE_CAM_IP_ADDRESS,'PixelFormat', 'BayerBG8'); - - % Change the ExposureTime setting (in us) - g.ExposureTimeAbs = 20000; - - % Acquire a single image from the camera - img = snapshot(g); - - % Clean up by clearing the object. - clear g; - else - % Just return a test image - img = imread('./Data/testImage13.bmp'); - end - - - -end - diff --git a/Modules/Func_Process_Images.m b/Modules/Func_Process_Images.m deleted file mode 100644 index 3d839b7..0000000 --- a/Modules/Func_Process_Images.m +++ /dev/null @@ -1,73 +0,0 @@ -function [sectionStatus] = Func_Process_Images(imageContainer) -%FUNC_PROCESS_IMAGES Process the images and returns status of each section -% 0 = Good Part (default, if no image given: log event) -% 1 = Bad Part - -% Step 0: Load Calibration & Evaluation Algorithm -c = Calibration('./Data/SandingImagesCalibration_Min.mat'); -classifier_data = load('./Data/RES101TrainedNET.mat'); -classifier_net = classifier_data.net; - -fprintf('%s Loading Images...\n', datestr(now,'HH:MM:SS.FFF')) -% Step 1: Load all the images -im_PoseID = []; -images = {}; - -i = 0; -for keys = imageContainer.keys - i=i+1; - im_PoseID(i) = keys{1}; - images{i} = imageContainer(keys{1}); -end - -fprintf('%s Masking and Cropping Images...\n', datestr(now,'HH:MM:SS.FFF')) -% Step 2: Mask and Crop all the images -% -masked_images = {}; -cropped_images = {}; -im_mask_PoseID = []; -im_mask_SecID = []; -i = 0; -for j = 1:length(images) - masks = c.calData{c.calData{:,'PoseID'}==im_PoseID(j),'Bitmask'}; - secIDs = c.calData{c.calData{:,'PoseID'}==im_PoseID(j),'SectionID'}; - for k = 1:length(masks) - i = i+1; - im_mask_PoseID(i) = im_PoseID(j); - im_mask_SecID(i) = secIDs(k); - % == Mask Image == - masked_images{i} = images{j}.*uint8(masks{k}); - % == Crop Image == - im = sum(masked_images{i},3); - % Flatten in horizontal and vertical dimensions, find uppermost and - % lowermost nonzero rows and columns - dim1 = sum(im,1); - dim2 = sum(im,2); - dim1_idx = [find(dim2,1,'first'),find(dim2,1,'last')]; - dim2_idx = [find(dim1,1,'first'),find(dim1,1,'last')]; - cropped_images{i} = masked_images{i}(dim1_idx(1):dim1_idx(2),dim2_idx(1):dim2_idx(2),:); - end -end - - -fprintf('%s Evaluating Images...\n', datestr(now,'HH:MM:SS.FFF')) -% Step 3: Evaluate all the images -% Initialize section status array. Index corresponds to PoseID now. -sectionStatus = logical(zeros(1,max(im_PoseID))); -checkedStatus = logical(zeros(1,max(im_PoseID))); -for i = 1:length(im_mask_PoseID) - [Predicted_Class, elapsed_prediction_time] = ... - RESClassifier(classifier_net,cropped_images{i}); - % Class 1 = Not Well Sanded (Bad Part) | Class 2 = Well Sanded (Good Part) - sectionStatus(im_mask_SecID(i)) = sectionStatus(im_mask_SecID(i)) || (Predicted_Class == '1'); - % - checkedStatus(im_mask_SecID(i)) = 1; -end -fprintf('%s Evaluation Complete.\n', datestr(now,'HH:MM:SS.FFF')) - - - - - -end - diff --git a/Modules/Func_Send_Capture_Complete.m b/Modules/Func_Send_Capture_Complete.m deleted file mode 100644 index fbd0540..0000000 --- a/Modules/Func_Send_Capture_Complete.m +++ /dev/null @@ -1,13 +0,0 @@ -function Func_Send_Capture_Complete(tcpConn) -%FUNC_SEND_ROBOT_MSG Signals the robotics system that the image has been -%captured. - -bits = [logical(zeros(1,63)) true]; - -% Convert bits to a uint8 (byte) array -msg = uint8(bi2de(reshape(bits,8,[])','left-msb'))'; - -% Send the message -fwrite(tcpConn,msg,'uint8'); -end - diff --git a/Modules/Func_Send_Section_Statuses.m b/Modules/Func_Send_Section_Statuses.m deleted file mode 100644 index 9839075..0000000 --- a/Modules/Func_Send_Section_Statuses.m +++ /dev/null @@ -1,17 +0,0 @@ -function Func_Send_Section_Statuses(tcpConn, sectionStatusRegister) -%FUNC_SEND_SECTION_STATUSES Sends sections statuses to robotics system - - -% Right-Pad the section status register to a length of 63, and left-pad -% with one zero (the camera done bit) -bits = [logical(zeros(1,63-length(sectionStatusRegister))), flip(sectionStatusRegister), false]; - -% Convert bits to a uint8 (byte) array -msg = uint8(bi2de(reshape(bits,8,[])','left-msb'))'; - -% Send the message -fwrite(tcpConn,msg,'uint8'); - - -end - diff --git a/Modules/Mod_ImageAcquisition.m b/Modules/Mod_ImageAcquisition.m new file mode 100644 index 0000000..a142c75 --- /dev/null +++ b/Modules/Mod_ImageAcquisition.m @@ -0,0 +1,51 @@ +classdef Mod_ImageAcquisition < handle + %MOD_IMAGEACQUISITION Image Acquisition Module + % This module is responsible for acquiring images from hardware + % devives. + + + %% Constants: Define Configuration Parameters Here (IP addresses, etc.) + properties (Constant) + camera_ip = '192.168.2.70'; + default_exposure_us = 20000; + end + + properties + % Camera Object + cam + + % Logger + log + end + + methods + function self = Mod_ImageAcquisition(logger) + %MOD_IMAGEACQUISITION Construct an instance of this class + + % Import the logger + self.log = logger; + + % Connect to the camera + self.log.info('Connecting to GigE Camera...'); + self.cam = gigecam(self.camera_ip, 'PixelFormat', 'BayerBG8'); + self.log.info('Connected to GigE Camera.'); + end + + function img = captureImage(self,curPoseData) + %CAPTUREIMAGE Captures an image(s) from the camera hardware + + % Set the camera exposure depending on the system state data + self.cam.ExposureTimeAbs = self.default_exposure_us; + pause(0.01); + + % Capture and return an image + img = snapshot(self.cam); + end + + function self = dispose(self) + self.cam = []; + end + + end +end + diff --git a/Modules/Mod_ImageProcessor.m b/Modules/Mod_ImageProcessor.m new file mode 100644 index 0000000..7982285 --- /dev/null +++ b/Modules/Mod_ImageProcessor.m @@ -0,0 +1,104 @@ +classdef Mod_ImageProcessor < handle + %MOD_IMAGEPROCESSOR Image Processing Module + % This module is responsible for processing the images and making a + % determination of the part status. + + + %% Constants: Define Configuration Parameters Here (IP addresses, etc.) + properties (Constant) + calibration_file_loc = './Data/SandingImagesCalibration_Min.mat'; + classifier_file_loc = './Data/RES101TrainedNET.mat'; + end + + properties + % Pose/Section/Bitmask Calibration + camera_part_calibration + + % Image Classifier + classifier + + % Logger + log + end + + methods + function self = Mod_ImageProcessor(logger) + %MOD_IMAGEPROCESSOR Construct an instance of this class + % Detailed explanation goes here + %addpath('Modules'); + % Import the Logger + self.log = logger; + + % Load Calibration and Evaluation Algorithm + self.log.info('Loading calibration data and image classifier...'); + self.camera_part_calibration = Calibration(self.calibration_file_loc); + classifier_data = load(self.classifier_file_loc); + self.classifier = classifier_data.net; + clear classifier_data; + self.log.info('Done loading calibration data and image classifier.'); + + end + + function statusData = processImages(self,imageContainer) + %PROCESSIMAGES Processes images, returns status of the part + + + % Step 1: Load all the images + self.log.info('Loading Images...'); + im_PoseID = []; + images = {}; + i = 0; + for keys = imageContainer.keys + i=i+1; + im_PoseID(i) = keys{1}; + images{i} = imageContainer(keys{1}); + end + + self.log.info('Masking and cropping images...'); + % Step 2: Mask and Crop all the images + % + masked_images = {}; + cropped_images = {}; + im_mask_PoseID = []; + im_mask_SecID = []; + i = 0; + for j = 1:length(images) + masks = self.camera_part_calibration.calData{self.camera_part_calibration.calData{:,'PoseID'}==im_PoseID(j),'Bitmask'}; + secIDs = self.camera_part_calibration.calData{self.camera_part_calibration.calData{:,'PoseID'}==im_PoseID(j),'SectionID'}; + for k = 1:length(masks) + i = i+1; + im_mask_PoseID(i) = im_PoseID(j); + im_mask_SecID(i) = secIDs(k); + % == Mask Image == + masked_images{i} = images{j}.*uint8(masks{k}); + % == Crop Image == + im = sum(masked_images{i},3); + % Flatten in horizontal and vertical dimensions, find uppermost and + % lowermost nonzero rows and columns + dim1 = sum(im,1); + dim2 = sum(im,2); + dim1_idx = [find(dim2,1,'first'),find(dim2,1,'last')]; + dim2_idx = [find(dim1,1,'first'),find(dim1,1,'last')]; + cropped_images{i} = masked_images{i}(dim1_idx(1):dim1_idx(2),dim2_idx(1):dim2_idx(2),:); + end + end + + self.log.info('Evaluating Images...'); + % Step 3: Evaluate all the images + % Initialize section status array. Index corresponds to PoseID now. + statusData = logical(zeros(1,max(im_PoseID))); + checkedStatus = logical(zeros(1,max(im_PoseID))); + for i = 1:length(im_mask_PoseID) + [Predicted_Class, elapsed_prediction_time] = ... + RESClassifier(self.classifier,cropped_images{i}); + % Class 1 = Not Well Sanded (Bad Part) | Class 2 = Well Sanded (Good Part) + statusData(im_mask_SecID(i)) = statusData(im_mask_SecID(i)) || (Predicted_Class == '1'); + checkedStatus(im_mask_SecID(i)) = 1; + end + % + self.log.info('Evaluation Complete'); + + end + end +end + diff --git a/Modules/Mod_ProcessControl.m b/Modules/Mod_ProcessControl.m new file mode 100644 index 0000000..733f3b1 --- /dev/null +++ b/Modules/Mod_ProcessControl.m @@ -0,0 +1,164 @@ +classdef Mod_ProcessControl < handle + %MOD_PROCESSCONTROL + + % Constants: Define Configuration Parameters Here (IP addresses, etc.) + properties (Constant) + IRC_IP_Address = 'localhost'; + IRC_IP_Port = 60451; + IRC_Packet_Terminator = 'CR'; + end + + properties + % TCP Connection + tcpConn + % Logger + log + + % Event Trigger Methods + method_begin_inspection + method_complete_inspection + method_capture_image + + % Data flow methods + method_updateState_CurrentPose + end + + methods + function self = Mod_ProcessControl(logger, methods) + %MOD_PROCESSCONTROL Construct an instance of this class + + % Import the Logger + self.log = logger; + + self.log.info('Initializing TCP/IP Connection to IRC...'); + % Begin TCP Communication with the IRC + self.tcpConn = tcpip(self.IRC_IP_Address, self.IRC_IP_Port); + self.log.info('IRC TCP/IP Connection Initialized.'); + + self.tcpConn.BytesAvailableFcn = {@self.msgReceived}; + self.tcpConn.BytesAvailableFcnMode = 'terminator'; + self.tcpConn.Terminator = self.IRC_Packet_Terminator; + + % Assign Trigger Methods + self.method_begin_inspection = methods.method_begin_inspection; + self.method_complete_inspection = methods.method_complete_inspection; + self.method_capture_image = methods.method_capture_image; + + % Assign Data Flow Methods + self.method_updateState_CurrentPose = methods.method_setState_CurrentPose; + + end + + function connect(self) + %CONNECT Opens a connection with the IRC, if not already open + self.log.info('Opening connection to IRC...'); + fopen(self.tcpConn); + self.log.info('Connection to IRC established.'); + end + + function disconnect(self) + %DISCONNECT Closes the connection with the IRC + fclose(self.tcpConn); + end + + function msgReceived(self,obj,event) + %MSGRECEIVED Handles reading incoming data from the IRC + + if(obj.BytesAvailable > 0) + % Read data from the buffer + msgData = fread(obj,obj.BytesAvailable); + % Pass the relevant bytes along (last byte is message terminator) + self.msgHandler(msgData(1:end-1)); + end + end + + function msgHandler(self,msgData) + %MSGHANDLER Dispatches actions based on incoming IRC messages + % Robot-To-PC Status Packet + % 0-Bit | 1-Bit | Name + % 0-7 1-8 PoseID + % 8 9 Robot Ready Bit + % 9 10 Start Inspection Bit + % 10 11 Inspection Complete Bit + + % Convert message bytes to bit array + msgBits = logical(reshape(de2bi(uint8(flip(msgData)),'right-msb',8)',[],1)'); + + % Acknowledge receipt of message + self.send_ack(); + self.log.info('Message from IRC received and Acknowledged.'); + + % Trigger events based on the incoming message data + + % Start Inspection Bit + if(msgBits(10)) + self.method_begin_inspection(); + + % Inspection Complete Bit + elseif(msgBits(11)) + self.method_complete_inspection(); + + % Robot Ready Bit (Capture Image) + elseif(msgBits(9)) + % Update the CurPoseID and Trigger Image Acquisition + poseData = struct(); + poseData.PoseID = uint8(msgData(2)); + self.method_updateState_CurrentPose(poseData); + self.method_capture_image(); + end + end + + function dispose(self) + self.disconnect(); + delete(self.tcpConn); + end + end + + % Process Control Response Methods + methods + % PC-to-Robot Packet Spec: + + + function self = send_section_statuses(self,sectionStatusRegister) + % SEND_SECTION_STATUSES Sends sections statuses to robotics system + self.send_PC2Robot_Packet(false,false,true,sectionStatusRegister); + end + + function self = send_capture_complete(self) + % SEND_CAPTURE_COMPLETE Signals the robotics system that the image + % has been captured. + self.send_PC2Robot_Packet(false,true,false,[]); + end + + function self = send_ack(self) + % SEND_ACK Signals the robotics system that the received message + % has been acknowledged + self.send_PC2Robot_Packet(true,false,false,[]); + end + + function send_PC2Robot_Packet(self, ack_bit, cameraDone_bit,sectionStatus_bit,sectionStatuses) + % SEND_PC2ROBOT_PACKET Sends data packet to IRC in predefined format + % Returned Packet: + % Bit 0 (ACK): 0 + % Bit 1 (Cam.Done): 0 + % Bit 2 (Sec.Stat): 1 + % Bit 3-63 (Statuses): Reported from sectionStatusRegister + + bits = [... + logical(ack_bit),... + logical(cameraDone_bit),... + logical(sectionStatus_bit),... + logical(sectionStatuses),... + logical(zeros(1,61-length(sectionStatuses)))]; + + % Convert bits to a uint8 (byte) array + msg = uint8(bi2de(reshape(flip(bits),8,[])','left-msb'))'; + + % Send the message + fwrite(self.tcpConn,msg,'uint8'); + + end + + end +end + diff --git a/RESClassifier.m b/Modules/RESClassifier.m similarity index 99% rename from RESClassifier.m rename to Modules/RESClassifier.m index 7033458..9a5a82a 100644 --- a/RESClassifier.m +++ b/Modules/RESClassifier.m @@ -1,3 +1,4 @@ + function [Predict_Class, TestTime] = RESClassifier(net, testImage) %% Create New Image Classification @@ -19,5 +20,4 @@ tic; Predict_Class = classify(net,Color_resize_testImage); TestTime = toc; -end - +end \ No newline at end of file diff --git a/Procedure_Draft.m b/Procedure_Draft.m deleted file mode 100644 index 6decbf6..0000000 --- a/Procedure_Draft.m +++ /dev/null @@ -1,64 +0,0 @@ - -%% Process Triggered By Remote Robot Controller - -% Trigger: Start Inspection Procedure Message Received -% Action: Send Acknowledged message to robot controller - -% Create a container to store all the captured images in -inspection_images = containers.Map('KeyType', 'single', 'ValueType', 'any'); - -while 1 - - % Block until triggered - % Trigger; Incoming Message - rcv_msg = 'tbd'; - - switch rcv_msg - case 'tbd' %[Robot Ready Bit] Call to defn. in - - % Here we need to get the pose. Should we have the robot - % communicate this to use, or just use a pre-defined set of - % poses? - pose = null; - - % Module: Camera Capture - % Input: None - % Output: Captured Image, 3xNxM uint8 - - % Action: Capture Image - img = Null; % [img] = modcam.captureImage() - - % Store the image in the container - inspection_images(pose) = img; - - - - case 'tbf' %[Inspection Complete Bit] Call to defn. in - break; - otherwise - % Throw and log error - end - - -end - -% Action: Evaluate the Images, Determine which subsections -% are bad -% This module should be connected to the calibration. - -% Module Action: Evaluation -% Input: A set of inspection images, list of poses that images -% were taken from -% Output: A list of sections, and good/bad part determination -section_status = zeros(1,8); - - - -% Action: Send section status register -% Module Action: Send Status -% Input: Column vector of status values: 0=Good, 1=Faulty -% Output: None - -% Complete - - diff --git a/README.md b/README.md index 53542cc..e4f19b7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Surface Inspection Integration -This is the first working version, containing all the code to communicate with the IRC, camera, and run the image processing algorithm. The architecture and organization of this software package will change significantly. +This software package acts as a client to an industrial robot controller, integrating a GigE camera and machine learning algorithm for automatic defect detection and reporting. -This early software can be tested with the IRC and Camera hardware with minimal setup. +The software architecture is in its current form in this version of the code, but will still require testing and (likely) bugfixes. @@ -13,8 +13,12 @@ This early software can be tested with the IRC and Camera hardware with minimal 4. Open MATLAB to the directory you cloned this repository to 5. Configure connections to the IRC and Camera, as described below. 6. Clear your MATLAB workspace: `clear all;` -7. Run `main.m` to start the program. The IRC will be automatically connected to, and the system will respond to packets from the IRC. -8. After testing, run `fclose(tcpConn)` to close the TCP connection to the IRC. +7. Start the inspection by instantiating the AutoOpticalInspection class: +``` +aoi = AutoOpticalInspection(); +``` +The IRC will be automatically connected to, and the system will respond to packets from the IRC. +8. **Important:** After testing, run `aoi.dispose();` to close the TCP connection to the IRC, the connection to the camera, and shut down the state machine. If this is not done, connections may persist in the background and cause errors when trying to restart the inspection. @@ -22,10 +26,10 @@ This early software can be tested with the IRC and Camera hardware with minimal ### Configuring IRC Connection -At the top of the `main.m` file, adjust the IRC IP address and Port number to match the actual IRC. +At the top of the `/Modules/Mod_ProcessControl.m` file, adjust the IRC IP address and Port number to match the actual IRC. ### Configuring the Camera Connection -In `Modules\Func_Capture_Image.m` modify the IP address for the camera. Setting the `test` variable to `0` will enable the use of the camera. When `test` is set to `1`, a testing image is loaded every time the capture image function is called. \ No newline at end of file +In `/Modules/Mod_ImageAcquisition.m` modify the IP address for the camera. The default exposure (shutter speed) can be adjusted here as well. \ No newline at end of file diff --git a/StateFlowChart.sfx b/StateFlowChart.sfx index 1d45039..93f1fca 100644 Binary files a/StateFlowChart.sfx and b/StateFlowChart.sfx differ diff --git a/devlog.txt b/devlog.txt new file mode 100644 index 0000000..0da6f19 --- /dev/null +++ b/devlog.txt @@ -0,0 +1,49 @@ + - Defined class for the integration, including: +(FIN) - Constructor +(FIN) - Instantiate Logging +(FIN) - Instantiate FSM +(FIN) - Instantiate Modules +(FIN) - FSM +(FIN) - Shutdown Method + - Module Classes for: + - Process Control Master (Robotics System) +(FIN) - Initialization +(FIN) - Startup (connects to system) +(FIN) - Trigger: Start Inspection Procedure +(FIN) - Trigger: Ready for Image Capture (with data) +(FIN) - Trigger: End Inspection Procedure +(FIN) - Action: Image Capture Complete +(FIN) - Action: Report Status (from data) +(FIN) - Shutdown (disconnects from system) +(FIN) - Disconnect Camera +(FIN) - Disconnect from IRC +(FIN) - Close everything +(FIN) - Destructor +(***) - Error Handling & Logging + - Image Acquisition +(FIN) - Initialization +(FIN) - Action: Acquire Image (with data) +(Add if needed) - Exposure Control? +(***) - Error Handling & Logging + - Image Processing +(FIN) - Initizlization +(FIN) - Action: Process Images +(***) - Log Unchecked Sections +(***) - Error Handling & Logging + - Develop Functionality For: +(FIN) - Logging +(***) - Live GUI +(IP) - Error Handling +(N/A) - Multiple Part Geometries (not needed?) +(***) - Separate (Standalone) Configuration File + + +Software Packages Used: + - https://github.com/optimizers/logging4matlab + +Known Bugs to be sorted: + - Intermittent loss of camera connection (need to figure out how to reproduce this first) + +Notes: + - Handle loss of connection to IRC + - Handle loss of connection to Camera diff --git a/msgHandler.m b/msgHandler.m deleted file mode 100644 index 8cfbdf4..0000000 --- a/msgHandler.m +++ /dev/null @@ -1,41 +0,0 @@ -function msgHandler(msgData,fsm) -%MSGHANDLER Handles Robot-To-PC Messages -% Robot-To-PC Status Packet -% 0-Bit | 1-Bit | Name -% 0-7 1-8 PoseID -% 8 9 Robot Ready Bit -% 9 10 Start Inspection Bit -% 10 11 Inspection Complete Bit - - % Convert message bytes to bit array - msgBits = logical(reshape(de2bi(uint8(flip(msgData)),'right-msb',8)',[],1)'); - - %% Trigger events based on the incoming message data - - % Start Inspection Bit - if(msgBits(10)) - fsm.ev_Req_Begin_Inspection(); - - % Inspection Complete Bit - elseif(msgBits(11)) - fsm.ev_Req_Complete_Inspection(); - - % Robot Ready Bit (Capture Image) - elseif(msgBits(9)) - % Pass the current PoseID to the FSM - fsm.curPoseID = uint8(msgData(2)); - fsm.ev_Req_Image_Capture(); - - % If none of the status bits are set, simply respond with an all-zeros - % message acknowledging receipt of the message. - else - bits = logical(zeros(1,64)); - msg = uint8(bi2de(reshape(bits,8,[])','left-msb'))'; - fwrite(fsm.dataStore.tcpConn,msg,'uint8'); - end - - - - -end -