User Guide ================================== This guide explains how to set up and run the Scorpion 2DScanner. To Scorpion the 2DScanner looks like an ordinary camera. You pick it from the camera list, it delivers one image per grab, and every tool in the toolbox works on that image as usual. What makes it a scanner is what happens between the trigger and the finished image: * several area scan cameras can feed the same scanner * every camera can be cropped and resampled to a calibrated, distortion free image * every camera can be shading corrected or contrast enhanced * every camera can be delayed in an image FIFO so images taken at different times line up * the results are stitched into one image - cameras side by side, scans stacked A 2DScanner built on 3D calibrated cameras is in effect a 3D scanning device. 3D calibration is also what makes several cameras line up cleanly when they are stitched - with 2D calibration only, seams are hard to avoid. Each camera is grabbed and processed in its own thread. Doing the resampling inside the driver rather than in the toolbox is faster, keeps the profile simpler and hides the camera geometry from the inspection. * the price is slightly higher latency and, in extreme cases, marginally lower frame rate .. note:: The 2DScanner runs on both **Scorpion (Windows)** and **SMARTedge (Linux ARM64)**. The same ``2DScanner.ini`` can be used on both. Windows ``.dll`` names for the camera driver, filter and resampler are translated to their Linux ``.so`` equivalents when the file is loaded - for example ``HVGrab_1_0_4_53.dll`` becomes ``libhvgrab.so``. Windows supports up to six scanners - ``Scanner0001`` .. ``Scanner0006``. SMARTedge supports one - ``Scanner0001``. How a scan is built --------------------------- Every image passes through the same four steps on its way from the camera to the finished scanner image: * **acquisition** - the image is fetched from the camera * **resampling** - the image is cropped and/or resampled into calibrated, real-world coordinates * **filtering** - shading correction or contrast enhancement is applied * **stitching** - the finished part-image is placed in the scanner image Where each part-image lands ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The scanner image is a grid. The **column** is given by the camera - camera 1 leftmost, camera 2 to the right of it, and so on. The **row** is given by the scan number - scan 1 on top, scan 2 below it, down to *Scans*. So the finished image is: * **width** = the widths of all cameras added together * **height** = the tallest camera image, multiplied by *Scans* .. note:: Cameras of **different width and height** may be combined. The images are placed side by side and padded down to the height of the tallest camera; the padding is black. Cameras with **different pixel depth** cannot be combined. The scanner refuses to open and writes a clear error to the console rather than delivering a corrupted image. If a scan times out before every camera has delivered, the missing part-images are filled with a fixed pixel value - see *ActiveGrabTimeout* and *MinScans*. x is down, y is across --------------------------- Throughout ``2DScanner.ini`` the suffix **_x** is the **vertical** direction and **_y** is the **horizontal** direction. This follows the Scorpion image convention and it is the most common source of confusion when setting up a scanner: ============ ============================================================ Suffix Meaning ============ ============================================================ ``_x`` down the image - **height**, the scanning direction ``_y`` across the image - **width**, the stitching direction ============ ============================================================ So ``Size_x`` is the height of one scan and ``Size_y`` its width, ``Pitch_x`` is the pixel height and ``Pitch_y`` the pixel width. ``TopLeft_y`` is what you change to move a camera sideways in the stitched image. Resampling --------------------------- Resampling converts the raw camera image into an image with real-world coordinates - millimetres rather than pixels - using a calibration file made in Scorpion. It removes lens distortion and perspective, so the object measures the same wherever it appears in the image. The work is done by **SVLResample.dll**, which is part of the standard Scorpion installation. The calibration file is usually produced by a separate Scorpion calibration profile using the same cameras, lenses and mounting as the running system. It defines an origin (0,0); *TopLeft* and *Size* are given relative to that origin, in object coordinates. Set *CalibFile* to the calibration file, either as an absolute path or relative to the folder holding ``2DScanner.ini``. Pixel mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Leave *CalibFile* empty - or set it to ``pixels`` - and the scanner runs in **pixel mode**: no calibration, no resampling, the camera image is used as it is. From version **1.1.0.31** pixel mode is much simpler: * the output size is taken straight from the camera image * no resampler plugin is loaded * *Size* and *TopLeft* are ignored Pixel mode is the right starting point when you just want to stack or stitch raw images, or while the calibration is still being built. Cropping ^^^^^^^^^^^^^^^^^^^^^^^^^^^ If the calibration was made on a *part* of the raw image rather than on the whole frame, tell the scanner about it with *CropOffset* and *CropSize*. These must match the sub image used in the calibration profile, otherwise the resampled image is offset. Dynamic ROI ^^^^^^^^^^^^^^^^^^^^^^^^^^^ *Centers* and *Angles* let the resampled area move and rotate from image to image, which is how you follow an object that shifts position between scans. Both are round-robin lists: the scanner steps to the next entry for every image and wraps around at the end. The position in the list is reset by the **reset** command. * *Centers* overrides *TopLeft_x* / *TopLeft_y* * *Angles* overrides *Angle* .. note:: Dynamic ROI adds resampling time on calibrated systems. In pixel mode with angle 0 a moving centre costs nothing extra. Filtering --------------------------- Each camera can have its own image filter, applied to every frame before it is stitched. The filter is created once, when the camera opens. Two filters are available: * **flat-field** (``f``) - shading correction from a calibration image * **CLAHE** (``h``) - local contrast enhancement Each camera section - and each calibration sub-section - can have its own filter settings. Filters work both in calibrated mode and in pixel mode. They are described in detail under `Image filters`_. `Example 19 `_ is a complete five camera system with calibration and shading correction on every camera, worked through step by step. Installation --------------------------- The 2DScanner is a *virtual* camera driver - it produces images, but it does not talk to hardware itself. It grabs from ordinary Scorpion camera drivers and delivers a stitched result. * installed by the Scorpion Vision Installer * used like any other camera driver * configured entirely from ``2DScanner.ini`` - there is no configuration dialog * pressing **Configure** on the scanner camera in Scorpion writes the current settings back to ``2DScanner.ini`` and shows you where the file is * up to six scanners in one profile on Windows, one on SMARTedge * the camera driver used for the real cameras is named in the configuration file .. note:: **Open the real cameras before the scanner.** A scanner can only attach to a camera port that is already open, so the physical cameras must come before ``Scanner0001`` in Scorpion's camera list. If a port is not open when the scanner starts, the console reports ``invalid port`` or ``invalid driver`` and that camera is left out of the scan. Set the physical cameras **inactive** and the scanner **active** in the image setup, unless you deliberately want the raw images as well. Configuration file --------------------------- The configuration file **2DScanner.ini** lives in the Scorpion profile's *Hardware* folder. If it is missing, create it by copying an example from this guide and editing it. The full path of the file actually in use can always be read back:: print GetCamera('Scanner0001').executeCmd('get','inifile') The file has three kinds of section: * ``[Config]`` - settings shared by every scanner in the file * ``[Scanner]`` - one per scanner, settings shared by its cameras * ``[Scanner.Camera]`` - one per camera in that scanner Settings cascade downwards. A value in ``[Config]`` is the default for every scanner, a value in ``[Scanner0001]`` is the default for all of its cameras, and a value in ``[Scanner0001.Camera2]`` applies to that camera alone. .. note:: Complete, commented examples are in the `Examples `_ chapter. Config section --------------------------- Settings shared by all scanners in the file. =============== ==================== ============================================================================== Key Default Description =============== ==================== ============================================================================== CameraDriver Scorpion Camera Driver used for the real cameras - the SCD FilterDLL SVLImageFilter.dll Scorpion standard image filter plugin ResampleDLL SVLResample.dll Scorpion standard resampling plugin ResampleMode 0 resampling quality / speed trade-off * 0 - bilinear * 1 - nearest neighbour * 2 - wide * 3 - fast wide Verbose 0 how much the driver writes to the console * 0 - errors and warnings only * 1 - and status messages * 2 - and scan-complete diagnostics * 3 - and per-image timing =============== ==================== ============================================================================== .. note:: *CameraDriver* must be the same for every camera attached to a 2DScanner. Example: ``HVGrab_1_0_4_53.dll`` - the SDK for that camera must be installed. .. note:: *FilterDLL*, *ResampleDLL* and *ResampleMode* may be overridden per scanner and per camera by repeating the key in the ``[Scanner]`` or ``[Scanner.Camera]`` section. This is rarely needed. *ResampleMode* defaults to bilinear, which gives the best image quality. Nearest neighbour is the fastest and is a good choice when the scan rate matters more than the last bit of accuracy. ``SVLResample.dll`` ships with Scorpion - changing *ResampleDLL* is not advised. .. note:: Raise *Verbose* while commissioning a system and lower it again for production. Level 3 prints one line per image and will itself slow down a fast scanner. Scanner section --------------------------- ``[Scanner]`` holds the settings shared by all cameras in one scanner. Every value here is the default for that scanner's cameras and can be overridden in an individual camera section. ================= ==================== ============================================================================== Key Default Description ================= ==================== ============================================================================== Cameras 1 number of cameras stitched side by side Scans 1 number of scans stacked to form the finished image ActiveGrabTimeout 0 ms allowed between images before the scan is declared timed out. 0 disables the timeout - the scanner waits indefinitely ImageDelay 0 ms to wait after a scan completes before handing it to Scorpion. A scan that completes while the delay is running is dropped and counted MinScans 0 how many scans may be missing and still deliver the image on timeout. See the note below Overlap 0 number of scans copied from the end of one image to the start of the next Scale_x 1.0 reserved - written to the file but currently ignored Scale_y 1.0 reserved - written to the file but currently ignored TopLeft_x 0 top left corner of the resampled area, in object coordinates relative to the calibration origin (0,0). Ignored in pixel mode TopLeft_y 0 as *TopLeft_x*, across the image. Ignored in pixel mode Size_x 100 height of one scan in object coordinates. Ignored in pixel mode, where the camera image height is used Size_y 100 width of one scan in object coordinates, shared between *Cameras* cameras. Ignored in pixel mode, where the camera image width is used Angle 0 rotation of the resampled area in degrees Pitch_x 1.0 pixel height in the finished image - object units per pixel Pitch_y 1.0 pixel width in the finished image - object units per pixel PassThru comma separated list of scan numbers whose raw camera image is also passed to Scorpion. Empty or '-' switches it off. **Windows only** - ignored on SMARTedge. Leave empty in runtime systems, see the note below Active 0 if 1, the stitched image is delivered before the raw pass-through image, otherwise after. Only relevant when *PassThru* is in use DiscardLostPacket 0 discard the scan if a camera reports a lost packet DiscardSeqError 0 discard the scan if a camera FrameNumber sequence gap is detected DiscardSyncError 0 discard the scan if the cameras FrameNumbers do not match each other Threaded 1 resample in a separate thread per camera. May be turned off for small images where the thread costs more than the resampling saves RingSize 32 per-camera capture buffer depth. The default absorbs short bursts so no frame is lost at high frame rates. Raised automatically to at least *Fifo* + 1 WorkerRtPriority 8 run the capture thread at real-time priority for smooth, jitter-free scanning. Set 0 for normal scheduling. See the note below ================= ==================== ============================================================================== .. note:: **Placing several cameras side by side.** When cameras are calibrated to a shared origin, set *TopLeft_x* / *TopLeft_y* and *Size_x* / *Size_y* **explicitly in each camera section**. The scanner does derive a default position for the next camera from the previous one, but relying on it makes the configuration hard to read and easy to get wrong. *Scale*, *TopLeft*, *Size*, *Pitch*, *Angle*, *Threaded*, *RingSize* and *WorkerRtPriority* may all be overridden per camera by repeating the key in the ``[Scanner.Camera]`` section. .. note:: **Overlap** copies the last *Overlap* scans of one image to the top of the next, so an object that straddles the boundary appears whole in at least one image. * useful when individual objects are scanned on a conveyor * the value is wrapped into the range 0 .. *Scans*-1 .. note:: **MinScans** only applies when *ActiveGrabTimeout* > 0. It is the number of scans that may be **missing** when the timeout strikes: * *MinScans* = 0 - the scan must be complete. Any partial scan is discarded. Use this when the images must be in sync, for example stereo vision * *MinScans* > 0 - up to that many scans may be missing. The image is delivered with the missing part filled black Timed-out scans are also marked visibly: the unfinished part of the image is filled with a black and white striped pattern so an operator can see at a glance that the image is incomplete. .. note:: **Scan validation** - *DiscardLostPacket*, *DiscardSeqError* and *DiscardSyncError* throw away a scan when a transport or synchronisation problem is detected, rather than passing on a torn or mismatched image: * *DiscardLostPacket* - a camera reported a lost packet (GigE transport layer) * *DiscardSeqError* - a camera FrameNumber sequence has a gap * *DiscardSyncError* - the cameras FrameNumbers are not in step with each other A discarded scan is reported on the console and counted in the metadata. .. note:: **High frame rate scanning** - *RingSize* and *WorkerRtPriority* keep the cameras running at full rate without falling behind (from version 1.1.0.32): * *RingSize* is the per-camera capture buffer depth. The default of 32 absorbs short bursts - about 0.6 s at 50 fps - so no frame is lost while a finished scan is handed to Scorpion. It is raised automatically to at least *Fifo* + 1 * *WorkerRtPriority* runs the capture thread at real-time priority for smooth, jitter-free scanning. Set 0 to use normal scheduling On Linux/SMARTedge real-time priority requires the real-time privilege. If it is not granted the driver logs a warning and continues at normal priority. .. note:: **PassThru** passes selected raw camera images to Scorpion alongside the stitched image. It is a commissioning aid - it lets you see what a camera actually delivered next to what the scanner made of it. The value is a comma separated list of scan numbers, in the style of Python indexing: * ``1,-1`` - the first and the last image of each scan * ``0`` - **every** image * ``1,4,8`` - those three * empty or ``-`` - pass-through off Counting starts at 1; negative numbers count back from the end. A number outside the range 1 .. *Scans* switches pass-through off, which is how ``setProperty('passthru', n)`` disables it. To see the raw images, the physical camera must also be **active** in the Scorpion image setup. *Active* on the scanner decides whether the stitched image arrives before or after the raw one. .. note:: **Pass-through is off unless you ask for it.** From version 1.1.0.33 the default is empty, so a scanner with no *PassThru* key passes no raw images. Leave it that way in runtime systems - every passed image is an extra image handed to Scorpion, which costs time and lowers the scan rate. Pass-through is **Windows only**. On SMARTedge the setting is ignored - the extra copy disturbs scan timing - and the driver says so in the log. Camera section --------------------------- ``[Scanner.Camera]`` holds the settings for one camera in the scanner. ```` counts from 1. ================== =================== ============================================================================== Key Default Description ================== =================== ============================================================================== Port -1 Scorpion camera port of the physical camera, **0-indexed**. This is the position of the camera in Scorpion's camera list, not a hardware port CalibFile calibration file for this camera - absolute, or relative to the folder holding ``2DScanner.ini``. Empty or ``pixels`` selects pixel mode. *TopLeft* and *Size* must match the calibration Fifo 0 number of images this camera is delayed before it is stitched. Use it to line up cameras that see the same object at different times. While the FIFO fills, this camera's part of the image is black. See the note below ImageFilterType f ``f`` flat-field / shading correction, ``h`` CLAHE contrast enhancement. No filter is created unless *ImageFilterArgs* or *ImageFilterOptions* is set ImageFilterArgs flat-field: path to the shading correction image. CLAHE: leave empty ImageFilterOptions filter options - see `Image filters`_ CropOffset_x 0 top of the crop rectangle in the raw image, in pixels CropOffset_y 0 left of the crop rectangle in the raw image, in pixels CropSize_x 0 crop height in pixels. 0 means no cropping CropSize_y 0 crop width in pixels. 0 means no cropping Angle 0 rotation of the resampled area in degrees Centers dynamic ROI centres, ``(x0,y0),...,(xn,yn)``. Stepped round-robin, one entry per image, reset by the **reset** command. Overrides *TopLeft* Angles dynamic ROI rotation, ``a0,...,an``. Stepped round-robin like *Centers* and reset by the same command. Overrides *Angle* ================== =================== ============================================================================== In addition, any of *TopLeft_x*, *TopLeft_y*, *Size_x*, *Size_y*, *Pitch_x*, *Pitch_y*, *Threaded*, *RingSize*, *WorkerRtPriority*, *ResampleMode*, *ResampleDLL* and *FilterDLL* may be repeated here to override the scanner-wide value for this camera. .. note:: **Fifo** delays a camera by a whole number of images so that cameras looking at the same object from different positions can be stitched together. * it lines images up in **time**, not in space * it needs the cameras to be triggered by the object movement - typically a hardware trigger from an encoder * while the FIFO is filling, this camera contributes black pixels * the **reset** command empties the FIFO .. note:: **Recommended port numbering** - Port=0 for camera 1, Port=1 for camera 2, and so on. Two cameras in a scanner *may* share a port when you want two different resampled areas out of one physical image, but duplicate ports otherwise cause confusion. .. note:: Shading correction images and calibration files are resolved relative to the folder holding ``2DScanner.ini``. Calibration files normally live in the profile's *Calibration/2D* folder. Several calibrations - working at more than one height -------------------------------------------------------- A 2D calibration is only valid at the height it was made. When objects arrive at different heights, make one calibration per height and switch between them at run time. Add extra sections named ``[Scanner.Camera.Calib]``, where ```` runs from 1 upwards. Repeat only the keys that differ from the parent camera section - normally *CalibFile*, and the filter keys if the shading correction also changes. * the parent ``[Scanner.Camera]`` section is calibration **0** * ``.Calib1`` is calibration **1**, ``.Calib2`` is calibration **2**, and so on Switch with the **calib** property, which applies to every camera in the scanner at once:: cam = GetCamera('Scanner0001') # get the scanner cam.setProperty('calib', 1) # switch every camera to calibration 1 .. note:: All cameras in a scanner switch together and must therefore have the same number of calibrations. Switching reloads the resampler and briefly interrupts scanning, so do it between scans, not during one. Worked examples: `Example 13 `_ switches between three calibration heights, `Example 15 `_ pairs each height with its own shading correction image. Image filters --------------------------- A filter cleans up each camera image before it is stitched. It is configured in the camera section - or in a ``Calib`` sub-section, so the filter can follow the calibration height. ====================== ============================================================ Key Description ====================== ============================================================ ``ImageFilterType`` Selects the filter. ``f`` = flat-field correction, ``h`` = CLAHE contrast enhancement. ``ImageFilterArgs`` Filter argument. For the flat-field filter this is the path to the calibration image. For CLAHE leave empty. ``ImageFilterOptions`` Option string controlling filter behaviour. See each filter section for the available options. ====================== ============================================================ .. note:: A filter is created as soon as **either** *ImageFilterArgs* or *ImageFilterOptions* has a value. To switch filtering off, clear **both** keys - or leave *ImageFilterType* empty. Omitting *ImageFilterType* is not enough on its own: it defaults to ``f``, the flat-field filter. .. note:: ``CalibFile`` in the same section is the geometric lens calibration used by the resampler. It is unrelated to the image filter. Option tokens in ``ImageFilterOptions`` may be written with spaces between them or run together - both forms are equivalent:: ImageFilterOptions = -t92 -fgray -g1.0 -o0 ImageFilterOptions = -t92-fgray-g1.0-o0 .. note:: The filter options changed in version **1.0.0.27**. The old ``PreFilter`` key is obsolete and is ignored - replace it with *ImageFilterType*, *ImageFilterArgs* and *ImageFilterOptions*. Flat-field filter (``f``) ----------------------------- Purpose ^^^^^^^ Corrects uneven illumination across the image - vignetting, lens fall-off and sensor non-uniformity. The correction is based on a calibration image captured under uniform illumination with the same lens and lighting as production scanning. Every pixel in the acquired frame is divided by the corresponding pixel in the calibration image, normalised to a target white-point value, and then an optional gain and offset are applied. The filter also handles Bayer-pattern cameras: if the source image is raw Bayer - single channel, 8- or 16-bit - and the output is colour (3 channels), demosaicing is performed automatically before the flat-field correction. Calibration image ^^^^^^^^^^^^^^^^^^ The calibration image should be a well-exposed flat-field image - typically a uniform white target or a diffuse light source - acquired with the same gain, exposure and gamma settings as production scanning. It is named by ``ImageFilterArgs``. The image can be any format supported by OpenCV (BMP, PNG, TIFF, ...). It is loaded once at startup. If the file is missing, or exists but cannot be read, an error is written to the console and the filter stays inactive. Options ^^^^^^^ ``-f`` Colour space to load the calibration image as. This must match the pixel format delivered by the camera: ========= ==================================================== Value Camera output format ========= ==================================================== ``gray`` 8-bit greyscale (1 channel) ``bgr`` 8-bit BGR colour (3 channels) ``rgb`` 8-bit RGB colour (3 channels) ``bgra`` 8-bit BGRA colour with alpha (4 channels) ``rgba`` 8-bit RGBA colour with alpha (4 channels) (omitted) Load unchanged - format as stored in the file ========= ==================================================== ``-t`` White-point target value. The calibration image is normalised so that this value corresponds to an output of the original pixel value - no correction applied. Pixels brighter than the target are corrected downward; darker pixels are corrected upward. * Default ``0`` - the driver uses the maximum pixel value found in the calibration image as the target. * Typical values: ``92`` for a sensor with a white-point near 92 counts; ``255`` for an 8-bit sensor; ``4095`` for a 12-bit sensor. ``-g`` Gain multiplier applied to the corrected image. Type: decimal number. Default: ``1.0``. Must be greater than zero. * ``1.0`` - no change in brightness. Use this when acquiring images for geometric calibration * ``0.5`` - halves output brightness, darkens the image * ``2.0`` - doubles output brightness, brightens the image ``-o`` Additive offset applied after gain. Type: decimal number. Default: ``0.0``. Range: ``-256`` to ``255``. Use it to shift the black level up or down. Configuration examples ^^^^^^^^^^^^^^^^^^^^^^^ **Greyscale camera - live acquisition vs. calibration context** Gain ``0.5`` during live scanning, ``1.0`` during calibration acquisition so the scanner receives unattenuated images for geometric calibration:: [Scanner0001.Camera1] Port = 0 ImageFilterType = f ImageFilterArgs = ..\Calibration\2D\grayscale\RightTop2D-Calib_Gain17_Gamma750_Expo1200.bmp ImageFilterOptions = -t92 -fgray -g0.5 -o0 [Scanner0001.Camera1.Calib1] ImageFilterType = f ImageFilterArgs = ..\Calibration\2D\grayscale\RightTop2D-Calib_Gain17_Gamma750_Expo1200.bmp ImageFilterOptions = -t92 -fgray -g1.0 -o0 **Colour BGR camera** No target override - the white-point is taken from the maximum pixel value in the calibration image:: [Scanner0001.Camera2] Port = 1 ImageFilterType = f ImageFilterArgs = ..\Calibration\2D\color\calib_bgr.bmp ImageFilterOptions = -fbgr -g1.0 -o0 [Scanner0001.Camera2.Calib1] ImageFilterType = f ImageFilterArgs = ..\Calibration\2D\color\calib_bgr.bmp ImageFilterOptions = -fbgr -g1.0 -o0 **12-bit Bayer camera to 8-bit BGR output** The camera delivers raw Bayer frames, which are demosaiced to BGR before the flat-field correction is applied. The ``-f`` option selects the Bayer pattern of the sensor - ``gr``, ``rg``, ``gb`` or ``bg``. The target is set to 4000 counts, typical for a well-exposed 12-bit flat image:: [Scanner0001.Camera3] Port = 2 ImageFilterType = f ImageFilterArgs = ..\Calibration\2D\bayer\calib_bgr.bmp ImageFilterOptions = -t4000 -fgb -g1.0 -o0 [Scanner0001.Camera3.Calib1] ImageFilterType = f ImageFilterArgs = ..\Calibration\2D\bayer\calib_bgr.bmp ImageFilterOptions = -t4000 -fgb -g1.0 -o0 Quick reference ^^^^^^^^^^^^^^^^ ====================== ======== ============================================== Option Default Effect ====================== ======== ============================================== ``-f`` (file) Camera pixel format / Bayer pattern: ``gray`` ``bgr`` ``rgb`` ``bgra`` ``rgba`` ``gr`` ``rg`` ``gb`` ``bg`` ``-t`` 0 White-point target; 0 = use calib image max ``-g`` 1.0 Brightness gain (must be > 0) ``-o`` 0.0 Black-level offset (-256 ... 255) ====================== ======== ============================================== CLAHE filter (``h``) ---------------------- Purpose ^^^^^^^ CLAHE - Contrast Limited Adaptive Histogram Equalisation - improves local contrast in images where the scene contains both bright and dark regions that cannot both be well exposed at the same time. Unlike a global brightness adjustment, CLAHE divides the image into small tiles and equalises the contrast within each tile independently, which prevents over-amplification of noise. The result is then mapped from the 16-bit working range to 8-bit output through a configurable lookup table (LUT). CLAHE does not use a calibration image. ``ImageFilterArgs`` must be left empty. Options ^^^^^^^ ``-c`` CLAHE clip limit. Controls how aggressively local contrast is stretched. Type: decimal number. Default: ``2.0``. * Higher values, for example ``4.0``, produce stronger contrast enhancement but can amplify noise * Lower values, for example ``1.0``, are more conservative * ``0`` disables CLAHE entirely - only the LUT is applied ``-s,`` Tile grid size for local histogram equalisation. Both values are integers. Default: ``8,8``. * Smaller tiles, for example ``4,4``, adapt more locally - better for images with rapid spatial variation in brightness * Larger tiles, for example ``16,16``, produce smoother, more global results * Setting either value to ``0`` disables CLAHE - LUT only ``-m`` Colour mode - which colour-space channel CLAHE is applied to. Only relevant for colour (3-channel) input. Ignored for greyscale. ========= ================================================================ Value Behaviour ========= ================================================================ ``v`` **HSV** - CLAHE on the Value channel only. Enhances brightness while preserving colour. *(default)* ``l`` **LAB** - CLAHE on the Lightness channel only. Similar to HSV but with better perceptual uniformity. ``r`` **RGB** - CLAHE applied independently to each colour channel. Stronger effect but may shift colour balance. ========= ================================================================ ``-l`` Lookup table mapping the 16-bit CLAHE result to 8-bit output. Format: ``,,...`` ```` is the maximum expected pixel value of the source image. Use ``4095`` for 12-bit cameras - most industrial cameras. Use ``65535`` for full 16-bit sensors. Three LUT shapes are available: ``g,`` - **Gamma curve** Maps the input range to 8-bit using a power function. ``gamma=1.0`` is a straight linear mapping - no contrast change. Values below 1.0 lift shadows; values above 1.0 compress them. Examples:: -lg4095,1.0 linear, 12-bit source (default) -lg4095,0.7 mild shadow lift -lg65535,1.0 linear, 16-bit source ``s,,`` - **Sigmoid S-curve** Creates a classic S-shaped tone curve that compresses extreme highlights and shadows while stretching mid-tones. ``contrast`` controls the steepness of the S - default ``10.0``. ``midpoint`` shifts the centre of the curve - 0 to 1, default ``0.5``. Examples:: -ls4095,8.0,0.5 moderate S-curve, centred -ls4095,12.0,0.4 strong S-curve, shifted toward shadows ``r,`` - **Reverse sigmoid, detail enhancer** A reverse S-curve that lifts shadows and compresses highlights at the same time, revealing detail at both ends of the tonal range. ``detail`` controls the strength - 0 to 1, default ``0.6``. Examples:: -lr4095,0.6 moderate detail enhancement -lr4095,0.8 strong detail enhancement Default if ``-l`` is omitted: ``g4095,1.0`` - linear gamma, 12-bit. Configuration examples ^^^^^^^^^^^^^^^^^^^^^^^ **12-bit mono camera - local contrast enhancement** Linear LUT with default CLAHE settings:: [Scanner0001.Camera4] Port = 3 ImageFilterType = h ImageFilterArgs = ImageFilterOptions = -lg4095,1.0 -c2.0 -s8,8 [Scanner0001.Camera4.Calib1] ImageFilterType = h ImageFilterArgs = ImageFilterOptions = -lg4095,1.0 -c2.0 -s8,8 **16-bit colour camera - HSV CLAHE with sigmoid LUT** Stronger CLAHE with finer tiles and a sigmoid LUT for better visual contrast in mid-tones:: [Scanner0001.Camera5] Port = 4 ImageFilterType = h ImageFilterArgs = ImageFilterOptions = -ls4095,8.0,0.5 -c3.0 -s16,16 -mv [Scanner0001.Camera5.Calib1] ImageFilterType = h ImageFilterArgs = ImageFilterOptions = -ls4095,8.0,0.5 -c3.0 -s16,16 -mv **LUT only - gamma compression without CLAHE** Disable CLAHE with ``-c0`` and apply only a gamma LUT to compress a 12-bit range to 8-bit with a mild shadow lift:: [Scanner0001.Camera6] Port = 5 ImageFilterType = h ImageFilterArgs = ImageFilterOptions = -lg4095,0.7 -c0 Quick reference ^^^^^^^^^^^^^^^^ ====================== ============ ========================================== Option Default Effect ====================== ============ ========================================== ``-c`` 2.0 CLAHE clip limit; ``0`` = LUT only ``-s,`` 8,8 Tile grid size H,W; ``0`` = LUT only ``-m`` v Colour mode: ``v`` HSV / ``l`` LAB / ``r`` RGB ``-l`` g4095,1.0 LUT shape and range - see above ====================== ============ ========================================== Running the scanner --------------------------- Everything the scanner does at run time is reached through the standard Scorpion camera interface - ``getProperty``, ``setProperty`` and ``executeCmd`` on the camera object, or the equivalent Scorpion commands:: cam = GetCamera('Scanner0001') Properties --------------------------- Properties can be read and written on the scanner itself. A property prefixed with **camera<1..n>.** is passed to that attached camera instead - for example ``camera1.exposure``. ====================== ======= ============= ============================================================================== Property Access Default Description ====================== ======= ============= ============================================================================== scanCount R/W 1 number of scans in the finished image - the *Scans* setting calib W 0 select calibration index for every camera at once. Write only - read the active file back with camera.resampler.calibfile count R/W - scans collected so far. Reset to 0 when a scan completes. Setting count>1 copies that many scans from the previous image to the top, creating an overlap overlap R/W 0 the *Overlap* setting minScanCount R/W 0 the *MinScans* setting activeGrabTimeout R/W 0 the *ActiveGrabTimeout* setting, in ms imageDelay R/W 0 the *ImageDelay* setting, in ms discardLostPacket R/W 0 the *DiscardLostPacket* setting discardSeqError R/W 0 the *DiscardSeqError* setting discardSyncError R/W 0 the *DiscardSyncError* setting continuous R/W 0 1 starts continuous scanning, 0 stops it. See `Continuous scanning`_ continuousDelay R/W 0 ms to wait after a finished scan before starting the next continuousPeriod R/W 0 ms between the start of one scan and the start of the next. 0 selects delay mode verbose R/W 0 console output level, 0-3. See the note below passthru W - pass-through scan number - the property form takes one number, not a list. A value outside 1 .. scanCount switches pass-through off pixelsize R 8 bits per pixel, mirrored from the camera width R - width of the finished image in pixels height R - height of the finished image in pixels cameras R - number of cameras configured for this scanner scanTime R - time from the first image of the scan until the scan completed, in ms trigTime R - average trigger period of the last scan, in ms activeGrabTimeoutCount R - number of timeouts since the scanner was opened camera. - - any property of attached camera *n* ====================== ======= ============= ============================================================================== .. note:: **Setting a property on the scanner sets it on every attached camera.** Any property the cameras understand - exposure, gain, trigger mode - can be set once on the scanner instead of camera by camera. This is both convenient and faster. **Reading** works the other way: a camera property must be read from one specific camera. :: cam = GetCamera('Scanner0001') # the scanner cam.setProperty('exposure', 1000) # sets exposure on every attached camera e = cam.getProperty('camera1.exposure', 1) # reads it back from camera 1 .. note:: **verbose** controls how much the driver writes to the Scorpion console: * 0 - errors and warnings only * 1 - and status messages * 2 - and one diagnostic line per finished scan * 3 - and one timing line per image Level 3 is the detailed image-flow view shown in Example 9 below. It costs time on a fast scanner - use it to investigate, not in production. Resampler and filter properties --------------------------------- Each camera exposes its resampler and filter through the same ``camera.`` prefix. Numeric values can be read with ``getProperty``; strings must be read with ``executeCmd('get', ...)``. ================================ ======= ============= ===================================================================== Property Access Default Description ================================ ======= ============= ===================================================================== camera.resampler.active R - 1 when this camera is resampling, 0 in pixel mode camera.resampler.height R - height of one scan from this camera, in pixels camera.resampler.width R - width of one scan from this camera, in pixels camera.resampler.calibfile R - path of the active calibration file - string, use executeCmd camera.filter.active R/W - 1 when the filter is on. Set 0 to bypass it camera.filter.targetvalue R/W 0 white-point of the shading correction. 0 uses **calibmax** camera.filter.calibmin R - lowest pixel value in the calibration image camera.filter.calibmax R - highest pixel value in the calibration image camera.filter.calibstatus R - ``ok``, ``missing`` or an error text - string, use executeCmd camera.filter.args R - the configured *ImageFilterArgs* - string, use executeCmd camera.filter.options R - the configured *ImageFilterOptions* - string, use executeCmd ================================ ======= ============= ===================================================================== :: cam = GetCamera('Scanner0001') print cam.executeCmd('get','camera1.resampler.calibfile') print cam.executeCmd('get','camera1.filter.calibstatus') cam.setProperty('camera1.filter.active', 0) # bypass the filter on camera 1 .. figure:: _images/consoleoutput.png :align: center :width: 650 **Console window with verbose messages** Commands --------------------------- Commands are sent with ``executeCmd(, )``. =============== ====================== ============================================================================= Command Parameters Description =============== ====================== ============================================================================= reset fill= abandon the current scan and start over. The scan counters are cleared and the camera FIFOs are emptied. With *fill* the image buffer is set to that pixel value, without it the buffer is left as it is. **flush** is an alias for **reset** trig fill= deliver the image now, even if the scan is incomplete. The remaining scans are filled with *fill*, which defaults to 0 - black. Pass ``fill=-1`` to leave the unfinished part of the buffer untouched clear fill= clear the image buffer of the incomplete scan. The pixel value is *fill*, or 0 if not given. Counters are not touched continuous start delay= start or stop continuous scanning. See `Continuous scanning`_ start period= stop status stat print a status report for the scanner to the console set = write any property, including all of those listed above get read any property as a string. Both ``get prop`` and ``get value=prop`` are accepted. In addition to the properties above: * ``inifile`` - full path of the active 2DScanner.ini * ``status`` or ``imageStatus`` - scanner counters as a Python dictionary string * ``meta`` - metadata for the finished scan, as JSON * ``timing`` - per-camera timing for the finished scan, as JSON * ``pitch`` - outgoing pixel size as ``(x,y)`` save save the current configuration as in the *Hardware* folder load load from the *Hardware* folder and restart the scanner with it =============== ====================== ============================================================================= .. note:: ``get inifile`` must be written exactly as ``executeCmd('get','inifile')`` - the ``value=`` form is not accepted for this one value. .. note:: ``save`` and ``load`` make it easy to keep several complete scanner setups in one profile and switch between them:: cam = GetCamera('Scanner0001') cam.executeCmd('save','2DScanner_highres.ini') # keep the current setup cam.executeCmd('load','2DScanner_fast.ini') # switch to another one ``load`` closes and reopens the scanner, so the image size may change. Continuous scanning --------------------------- From version **1.1.0.32** the scanner can run by itself, without a trigger from a script or an external signal. It starts the next scan as soon as the previous one is finished, which is the simplest way to get a live scanning image while setting a system up. Two timing modes are available: * **delay** - wait *delay* ms after a scan completes, then start the next one. The scan rate follows whatever the cameras and processing can manage * **period** - start a new scan every *period* ms, measured from the start of the previous scan. Use this when the scan rate must be constant. If a scan overruns the period, the next one starts immediately Start and stop it with a command:: cam = GetCamera('Scanner0001') cam.executeCmd('continuous','start delay=100') # 100 ms between scans cam.executeCmd('continuous','start period=500') # a scan every 500 ms cam.executeCmd('continuous','stop') print cam.executeCmd('continuous','status') # {'continuous': 1, 'delay': 100, 'period': 0} or with properties:: cam.setProperty('continuousDelay', 100) # preset the timing cam.setProperty('continuous', 1) # start cam.setProperty('continuous', 0) # stop .. note:: *period* takes precedence over *delay*. Set *continuousPeriod* to 0 to use delay mode. Continuous mode keeps running until it is stopped or the scanner is closed. Stop it before switching calibration or loading another configuration. .. warning:: The scanner accepts both spellings, ``continuous`` and ``continous``, for this property - and both are handled **by the scanner**, not passed on to the cameras. Many camera drivers use ``continous`` for their own continuous grab mode. From 1.1.0.32, setting it on the scanner starts continuous *scanning* instead. To put a physical camera into continuous grab, address it directly:: cam.setProperty('camera1.continous', 1) # the camera, not the scanner Metadata and timing --------------------------- Every finished scan carries metadata describing how it was acquired. It is delivered to Scorpion with the image - available on ``CameraImage()`` instances and through ``Central.CameraCommand`` - and can also be read back on demand. :: cam = GetCamera('Scanner0001') print cam.executeCmd('get','meta') =============== ============================================================================== Field Description =============== ============================================================================== span duration of the scan in ms, first image to last imagenumber running count of good scans since the scanner was opened timeout running count of timed-out scans drops running count of scans dropped because *ImageDelay* was still running lostpacket total lost packets over all cameras and scans in this image framenumber the camera FrameNumber of every image in the scan, one list per camera frameperiod time between consecutive images in ms, one list per camera framerate average frame rate per camera, in frames per second std standard deviation of the frame rate per camera - a stability measure =============== ============================================================================== Use *framerate* and *std* to confirm the cameras are running at the expected rate and staying in step. A *std* that grows, or *lostpacket* counting up, points at a transport or bandwidth problem rather than an optical one. **timing** breaks the same scan down by processing step, all values in ms:: print cam.executeCmd('get','timing') {"scan":1657,"meta":0.4,"callback":2.1, "cameras":[{"collect":2.7,"resample":5.1,"filter":0.0,"queue":0.2,"process":8.0}]} =============== ============================================================================== Field Description =============== ============================================================================== scan total scan duration meta time spent delivering the metadata to Scorpion callback time Scorpion spent taking the finished image collect fetching the image from the camera resample resampling filter filtering - shading correction or CLAHE queue waiting between collection and the start of processing process total processing time for that camera =============== ============================================================================== A large *queue* time means the processing cannot keep up with the cameras - lower the frame rate, turn off filtering, or switch *ResampleMode* to nearest neighbour. A large *callback* time means Scorpion itself is the bottleneck. Examples - properties --------------------------- Example 1 - set properties for all cameras at once ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: cam = GetCamera('Scanner0001') # the scanner cam.setProperty('exposure', 1000) # exposure on every attached camera cam.setProperty('gain', 4) # gain on every attached camera Example 2 - read a property from one camera ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: exposure = cam.getProperty('camera1.exposure', 1) # via the scanner exposure = GetCamera('0').getProperty('exposure') # directly, same result Example 3 - change the scan setup at run time ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: cam = GetCamera('Scanner0001') cam.setProperty('scanCount', 6) # 6 scans per image cam.setProperty('activeGrabTimeout', 500) # give up after 500 ms Example 4 - switch calibration height ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use this when the configuration holds several calibrations, typically one per working height. The switch applies to every camera in the scanner. :: cam = GetCamera('Scanner0001') # the scanner cam.setProperty('calib', 1) # switch to calibration 1 Examples - scripts --------------------------- Example 5 - scanner status for several scanners ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: def imagestatus(): cameras = ('Scanner0001','Scanner0002') for cam in cameras: print cam, GetCamera(cam).executeCmd('get','imageStatus') # output [10:41:13:950] Scanner0001 {'imageCount': 2061, 'activeGrabTimeoutCount': 0,'activeGrabTime': 1577, 'trigTime': 65, 'camera0': {'imageCount': 49464, 'activeGrabTimeoutCount': 0}} [10:41:13:950] Scanner0002 {'imageCount': 2061, 'activeGrabTimeoutCount': 0,'activeGrabTime': 1577, 'trigTime': 65, 'camera0': {'imageCount': 49464, 'activeGrabTimeoutCount': 0}} Example 6 - pick the status apart ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The status is returned as a string, so ``eval`` turns it into a dictionary. Note that the per-camera keys are **camera0, camera1, ...** - 0-indexed - while the property prefix ``camera.`` counts from 1. :: def imagestatus(): # several cameras on one scanner print 50*'-' status = eval(GetCamera('Scanner0001').executeCmd('get','imageStatus')) print 'imcnt:',status['imageCount'], 'aGrabTimeoutCnt:',status['activeGrabTimeoutCount'], print 'activeGrabTime:',status['activeGrabTime'], 'trigTime:',status['trigTime'] print 'cam1:',status['camera0'] print 'cam2:',status['camera1'] print 'cam3:',status['camera2'] # output imcnt: 1855 aGrabTimeoutCnt: 0 activeGrabTime: 1891 trigTime: 210 cam1: {'activeGrabTimeoutCount': 2, 'imageCount': 16707} cam2: {'activeGrabTimeoutCount': 2, 'imageCount': 16707} cam3: {'activeGrabTimeoutCount': 2, 'imageCount': 16707} Example 7 - print a status report ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :: GetCamera('Scanner0001').executeCmd('stat') # writes to the console # console output --------------------------------------------------- Scanner0001: last scan 1657 ms, avg trig period 184 ms Image count 690 timeout count 0 drop count 0 Cam[1] image count 0 total 6218 timeouts 1 Cam[2] image count 0 total 6218 timeouts 1 Cam[3] image count 0 total 6218 timeouts 1 Example 8 - start a scan from scratch ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Clears the counters, empties the camera FIFOs and blanks the image, so the next scan starts from a known state. :: cam = GetCamera('Scanner0001') cam.executeCmd('reset','fill=0') Example 9 - watch the image flow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Verbose level 3 prints one line per image with a breakdown of where the time goes. It is the quickest way to see which step is holding a scanner back. :: def verbose(): cam = GetCamera('Scanner0001') if cam.getProperty('verbose') < 3: cam.setProperty('verbose',3) else: cam.setProperty('verbose',0) # sample output [09:36:55:311] Scanner0001.0 [1/8] 2.7 | 5.1 | 0.0 | 0.2 | 8.0 ms [09:36:55:311] Scanner0001.1 [1/8] 0.3 | 5.2 | 0.0 | 0.2 | 5.7 ms [09:36:55:312] Scanner0001.0 [2/8] 0.2 | 4.6 | 0.0 | 0.6 | 5.4 ms [09:36:55:312] Scanner0001.1 [2/8] 0.2 | 5.1 | 0.0 | 0.2 | 5.5 ms * * * * * * * 1 2 3 4 5 6 7 * 1 - source, scanner name and camera port * 2 - progress, scan number of scanCount * 3 - collect time - fetching the image from the camera * 4 - resample time * 5 - filter time * 6 - queue time - waiting between collection and start of processing * 7 - total processing time At ``verbose=2`` the scanner prints one line per finished scan instead, with the per-camera frame rate and its standard deviation - enough to confirm the cameras are in step without the per-image detail.