diff --git a/.gitignore b/.gitignore
index ad90b97..5ce8b8d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,4 @@ stats.json
# dev environments
.vscode/*
**/.DS_Store
+test_images/*.jpeg
diff --git a/.npmignore b/.npmignore
index 87e583d..543b066 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,4 +1,5 @@
src/
+test_images/
yarn.lock
tsconfig.json
diff --git a/README.md b/README.md
index 4bd419a..8c0a793 100644
--- a/README.md
+++ b/README.md
@@ -230,6 +230,8 @@ Note that this example produces a raw H264 video. Wrapping it in a video contain
- [`SensorMode`](#sensormode)
- [`ExposureMode`](#exposuremode)
- [`AwbMode`](#awbmode)
+- [`DynamicRange`](#dynamicRange)
+- [`ImageEffectMode`](#imageEffectMode)
## `StillCamera`
@@ -261,8 +263,25 @@ const stillCamera = new StillCamera({
- `exposureCompensation: number` - _Range: `-10`-`10`; Default: `0`_
- [`exposureMode: ExposureMode`](#exposuremode) - _Default: `ExposureMode.Auto`_
- [`awbMode: AwbMode`](#awbmode) - _Default: `AwbMode.Auto`_
-- `analogGain: number` - _Default: `0`_
-- `digitalGain: number` - _Default: `0`_
+- `awbGains: [number, number]` - _Range: `0.0`-`8.0`; Default: `undefined`_
+- `analogGain: number` - _Range: `1.0`-`12.0` (OV5647: `1.0`-`8.0`); Default: `0`_
+- `digitalGain: number` - _Range: `1.0`-`64.0`; Default: `0`_
+- `quality: number` - _Range: `0`-`100`; Default: `100`_
+- `colorEffect: [number, number]` (U, V) - _Range: `0-255`; Default: `undefined`_
+- [`imageEffectMode: ImageEffectMode`](#imageeffectmode) - _Default: `ImageEffectMode.None`_
+- [`dynamicRange: DynamicRange`](#dynamicrange) - _Default: `DynamicRange.Off`_
+- `videoStabilization: boolean` - _Default: `false`_
+- `raw: boolean` - _Default: `false`_
+- [`meteringMode`](#meteringMode) - _Default: `MeteringMode.Off`_
+- `thumbnail: [number, number, number] | false` (X, Y, Q) - _Default: `[64, 48, 35]`; `false` to dismiss thumbnail_
+- [`flickerMode`](#flickerMode) - _Default: `FlickerMode.Off`_
+- `burst: boolean` - _Default: `false`_
+- `roi: [number, number, number, number]` (X, Y, W, H) - _Range: `0.0`-`1.0`; Default: `[0, 0, 1, 1]` (Full sensor)_
+- `statistics: boolean` - _Default: `false`_
+- `exif: { [key:string]: string | number } | false` - _Default: Default camera values; `false` to dissmis default exif_
+- `gpsExif: boolean` - _Default: `false`_
+- `annotate: (number | string)[]` - _Default: No annotations_
+- `annotateExtra: [number, string, string]` (fontSize, fontColor, backgroundColor) - _Default: `[32, '0xff', '0x808000']`_
### `StillCamera.takeImage(): Promise`
@@ -307,8 +326,19 @@ const streamCamera = new StreamCamera({
- `exposureCompensation: number` - _Range: `-10`-`10`; Default: `0`_
- [`exposureMode: ExposureMode`](#exposuremode) - _Default: `ExposureMode.Auto`_
- [`awbMode: AwbMode`](#awbmode) - _Default: `AwbMode.Auto`_
-- `analogGain: number` - _Default: `0`_
-- `digitalGain: number` - _Default: `0`_
+- `awbGains: [number, number]` - _Range: `0.0`-`8.0`; Default: `undefined`_
+- `analogGain: number` - _Range: `1.0`-`12.0` (OV5647: `1.0`-`8.0`); Default: `0`_
+- `digitalGain: number` - _Range: `1.0`-`64.0`; Default: `0`_
+- `colorEffect: [number, number]` (U, V) - _Range: `0-255`; Default: `undefined`_
+- [`imageEffectMode: ImageEffectMode`](#imageeffectmode) - _Default: `ImageEffectMode.None`_
+- [`dynamicRange: DynamicRange`](#dynamicrange) - _Default: `DynamicRange.Off`_
+- `videoStabilization: boolean` - _Default: `false`_
+- [`meteringMode`](#meteringMode) - _Default: `MeteringMode.Off`_
+- [`flickerMode`](#flickerMode) - _Default: `FlickerMode.Off`_
+- `roi: [number, number, number, number]` (X, Y, W, H) - _Range: `0.0`-`1.0`; Default: `[0, 0, 1, 1]` (Full sensor)_
+- `statistics: boolean` - _Default: `false`_
+- `annotate: (number | string)[]` - _Default: No annotations_
+- `annotateExtra: [number, string, string]` (fontSize, fontColor, backgroundColor) - _Default: `[32, '0xff', '0x808000']`_
### `startCapture(): Promise`
@@ -446,7 +476,19 @@ These are slightly different depending on the version of Raspberry Pi camera you
| 4 | 1640x1232 | 4:3 | 0.1-40fps | Full | 2x2 |
| 5 | 1640x922 | 16:9 | 0.1-40fps | Full | 2x2 |
| 6 | 1280x720 | 16:9 | 40-90fps | Partial | 2x2 |
-| 7 | 640x480 | 4:3 | 40-90fps | Partial | 2x2 |
+| 7 | 640x480 | 4:3 | 40-200fps* | Partial | 2x2 |
+
+*For frame rates over 120fps, it is necessary to turn off automatic exposure and gain control using -ex off. Doing so should achieve the higher frame rates, but exposure time and gains will need to be set to fixed values supplied by the user.
+
+#### HQ Camera (IMX477):
+
+| Mode | Size | Aspect Ratio | Frame rates | FOV | Binning |
+|------|---------------------|--------------|-------------|---------|-------------|
+| 0 | automatic selection | | | | |
+| 1 | 2028x1080 | 169:90 | 0.1-50fps | Partial | 2x2 binned |
+| 2 | 2028x1520 | 4:3 | 0.1-50fps | Full | 2x2 binned |
+| 3 | 4056x3040 | 4:3 | 0.005-10fps | Full | None |
+| 4 | 1332x990 | 74:55 | 50.1-120fps | Partial | 2x2 binned |
## `ExposureMode`
@@ -489,3 +531,71 @@ White balance mode options.
```javascript
import { AwbMode } from 'pi-camera-connect';
```
+
+## `ImageEffectMode`
+
+Image Effect options.
+
+- `ImageEffectMode.None`
+- `ImageEffectMode.Negative`
+- `ImageEffectMode.Solarise`
+- `ImageEffectMode.Sketch`
+- `ImageEffectMode.Denoise`
+- `ImageEffectMode.Emboss`
+- `ImageEffectMode.OilPaint`
+- `ImageEffectMode.Hatch`
+- `ImageEffectMode.GPen`
+- `ImageEffectMode.Pastel`
+- `ImageEffectMode.Watercolour`
+- `ImageEffectMode.Film`
+- `ImageEffectMode.Blur`
+- `ImageEffectMode.Saturation`
+- `ImageEffectMode.ColourSwap`
+- `ImageEffectMode.WashedOut`
+- `ImageEffectMode.Posterise`
+- `ImageEffectMode.ColourPoint`
+- `ImageEffectMode.ColourBalance`
+- `ImageEffectMode.Cartoon`
+
+```javascript
+import { ImageEffectMode } from 'pi-camera-connect';
+```
+
+## `DynamicRange`
+
+Dynamic Range options.
+
+- `DynamicRange.Off`
+- `DynamicRange.Low`
+- `DynamicRange.Medium`
+- `DynamicRange.High`
+
+```javascript
+import { DynamicRange } from 'pi-camera-connect';
+```
+
+## `MeteringMode`
+
+Dynamic Range options.
+
+- `MeteringMode.Average`
+- `MeteringMode.Spot`
+- `MeteringMode.Backlit`
+- `MeteringMode.Matrix`
+
+```javascript
+import { MeteringMode } from 'pi-camera-connect';
+```
+
+## `FlickerMode`
+
+Dynamic Range options.
+
+- `FlickerMode.Off`
+- `FlickerMode.Auto`
+- `FlickerMode.Frq50hz`
+- `FlickerMode.Frq60hz`
+
+```javascript
+import { FlickerMode } from 'pi-camera-connect';
+```
diff --git a/src/index.ts b/src/index.ts
index b35ddd9..58ddf71 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -44,3 +44,47 @@ export enum AwbMode {
Horizon = 'horizon',
GreyWorld = 'greyworld',
}
+
+export enum ImageEffectMode {
+ None = 'none',
+ Negative = 'negative',
+ Solarise = 'solarise',
+ Sketch = 'sketch',
+ Denoise = 'denoise',
+ Emboss = 'emboss',
+ OilPaint = 'oilpaint',
+ Hatch = 'hatch',
+ GPen = 'gpen',
+ Pastel = 'pastel',
+ Watercolour = 'watercolour',
+ Film = 'film',
+ Blur = 'blur',
+ Saturation = 'saturation',
+ ColourSwap = 'colourswap',
+ WashedOut = 'washedout',
+ Posterise = 'posterise',
+ ColourPoint = 'colourpoint',
+ ColourBalance = 'colourbalance',
+ Cartoon = 'cartoon',
+}
+
+export enum DynamicRange {
+ Off = 'off',
+ Low = 'low',
+ Medium = 'medium',
+ High = 'high',
+}
+
+export enum MeteringMode {
+ Average = 'average',
+ Spot = 'spot',
+ Backlit = 'backlit',
+ Matrix = 'matrix',
+}
+
+export enum FlickerMode {
+ Off = 'off',
+ Auto = 'auto',
+ Frq50hz = '50hz',
+ Frq60hz = '60hz',
+}
diff --git a/src/lib/shared-args.ts b/src/lib/shared-args.ts
index aa02fba..50aeadc 100644
--- a/src/lib/shared-args.ts
+++ b/src/lib/shared-args.ts
@@ -66,12 +66,12 @@ export function getSharedArgs(options: StillOptions | StreamOptions): string[] {
...(options.saturation ? ['--saturation', options.saturation.toString()] : []),
/**
- * ISO
+ * ISO (100 to 800)
*/
...(options.iso ? ['--ISO', options.iso.toString()] : []),
/**
- * EV Compensation
+ * EV Compensation (-10 to 10; default 0)
*/
...(options.exposureCompensation ? ['--ev', options.exposureCompensation.toString()] : []),
@@ -85,14 +85,98 @@ export function getSharedArgs(options: StillOptions | StreamOptions): string[] {
*/
...(options.awbMode ? ['--awb', options.awbMode.toString()] : []),
+ /**
+ * Sets the blue and red channel gains if awbMode is Off
+ */
+ ...(options.awbGains ? ['--awbgains', options.awbGains.toString()] : []),
+
/**
* Analog Gain
+ * Sets the analog gain value directly on the sensor (floating point value from
+ * 1.0 to 8.0 for the OV5647 sensor on Camera Module V1, and 1.0 to 12.0 for the
+ * IMX219 sensor on Camera Module V2 and the IMX447 on the HQ Camera).
*/
...(options.analogGain ? ['--analoggain', options.analogGain.toString()] : []),
/**
* Digital Gain
+ * Sets the digital gain value applied by the ISP
+ * (floating point value from 1.0 to 64.0,
+ * but values over about 4.0 willproduce overexposed images)
*/
...(options.digitalGain ? ['--digitalgain', options.digitalGain.toString()] : []),
+
+ /**
+ * Image Effect
+ */
+ ...(options.imageEffectMode ? ['--imxfx', options.imageEffectMode.toString()] : []),
+
+ /**
+ * Dynamic Range Control
+ */
+ ...(options.dynamicRange ? ['--drc', options.dynamicRange] : []),
+
+ /**
+ * Color Effects
+ * The supplied U and V parameters (range 0 to 255) are applied to
+ * the U and Y channels of the image. For example, --colfx 128:128
+ * should result in a monochrome image.
+ */
+ ...(options.colorEffect ? ['--colfx', options.colorEffect.join(':')] : []),
+
+ /**
+ * Metering
+ * Specify the metering mode used for the preview and capture.
+ */
+ ...(options.meteringMode ? ['--metering', options.meteringMode] : []),
+
+ /**
+ * Flicker Avoid Mode
+ * Set a mode to compensate for lights flickering at the mains frequency,
+ * which can be seen as a dark horizontal band across an image.
+ * Flicker avoidance locks the exposure time to a multiple of the mains
+ * flicker frequency (8.33ms for 60Hz, or 10ms for 50Hz).
+ * This means that images can be noisier as the control algorithm has to
+ * increase the gain instead of exposure time should it wish for an
+ * intermediate exposure value. auto can be confused by external factors,
+ * therefore it is preferable to leave this setting off unless actually required.
+ */
+ ...(options.flickerMode ? ['--flicker', options.flickerMode] : []),
+
+ /**
+ * Video Stabilization
+ * In video mode only, turn on video stabilization.
+ */
+ ...(options.videoStabilization ? ['--vstab'] : []),
+
+ /**
+ * Statistics
+ * Force recomputation of statistics on stills capture pass. Digital gain and AWB are
+ * recomputed based on the actual capture frame statistics,
+ * rather than the preceding preview frame.
+ */
+ ...(options.statistics ? ['--stats'] : []),
+
+ /**
+ * Sensor region of interest
+ * Allows the specification of the area of the sensor to be used as
+ * the source for the preview and capture. This is defined as x,y for
+ * the top left corner, and a width and height, all values in
+ * normalised coordinates (0.0-1.0).
+ */
+ ...(options.roi ? ['--roi', options.roi.toString()] : []),
+
+ /**
+ * Annotate
+ * Adds some text and/or metadata to the picture.
+ */
+ ...(options.annotate ? options.annotate.flatMap(e => ['--annotate', e.toString()]) : []),
+
+ /**
+ * Annotate extra
+ * Specifies annotation size, text colour, and background color.
+ * Colors are in hex YUV format. Size ranges from 6 - 160; default is 32
+ */
+ ...(options.annotateExtra ? ['--annotateex', options.annotateExtra.toString()] : []),
];
}
diff --git a/src/lib/still-camera.test.ts b/src/lib/still-camera.test.ts
index 9485e6a..a71d227 100644
--- a/src/lib/still-camera.test.ts
+++ b/src/lib/still-camera.test.ts
@@ -1,9 +1,41 @@
+import * as fs from 'fs';
+import { performance } from 'perf_hooks';
+import { ImageEffectMode } from '..';
+
import StillCamera from './still-camera';
+const TEST_IMAGES_DIR = 'test_images';
+
+if (!fs.existsSync(TEST_IMAGES_DIR)) {
+ fs.mkdirSync(TEST_IMAGES_DIR);
+}
+
test('takeImage() returns JPEG', async () => {
- const stillCamera = new StillCamera();
+ const t0 = performance.now();
+
+ const stillCamera = new StillCamera({
+ imageEffectMode: ImageEffectMode.Sketch,
+
+ // 2X zoom
+ roi: [0.25, 0.25, 0.5, 0.5],
+
+ // Size 50 black text on white background
+ annotateExtra: [50, '0x00', '0x8080FF'],
+
+ // Custom text and Date/Time
+ annotate: [4, 'pi-camera-connect %Y-%m-%d %X'],
+
+ exif: {
+ 'IFD0.Artist': 'pi-camera-connect',
+ 'IFD0.ImageDescription': 'This is a custom description',
+ },
+ });
const jpegImage = await stillCamera.takeImage();
+ const t1 = performance.now();
+
+ const time = ((t1 - t0) / 1000).toFixed(2);
+ await fs.promises.writeFile(`test_images/stillCapture_(${time}-secs).jpeg`, jpegImage, 'binary');
expect(jpegImage.indexOf(StillCamera.jpegSignature)).toBe(0);
});
diff --git a/src/lib/still-camera.ts b/src/lib/still-camera.ts
index df7ee14..77ed62b 100644
--- a/src/lib/still-camera.ts
+++ b/src/lib/still-camera.ts
@@ -1,4 +1,13 @@
-import { AwbMode, ExposureMode, Flip, Rotation } from '..';
+import {
+ AwbMode,
+ DynamicRange,
+ ExposureMode,
+ FlickerMode,
+ Flip,
+ ImageEffectMode,
+ MeteringMode,
+ Rotation,
+} from '..';
import { spawnPromise } from '../util';
import { getSharedArgs } from './shared-args';
@@ -17,8 +26,25 @@ export interface StillOptions {
exposureCompensation?: number;
exposureMode?: ExposureMode;
awbMode?: AwbMode;
+ awbGains?: [number, number];
analogGain?: number;
digitalGain?: number;
+ imageEffectMode?: ImageEffectMode;
+ colorEffect?: [number, number]; // U,V
+ dynamicRange?: DynamicRange;
+ videoStabilization?: boolean;
+ raw?: boolean;
+ quality?: number;
+ statistics?: boolean;
+ thumbnail?: [number, number, number] | false; // X, Y, Q
+ meteringMode?: MeteringMode;
+ flickerMode?: FlickerMode;
+ burst?: boolean;
+ roi?: [number, number, number, number]; // X, Y, W, H
+ exif?: { [key: string]: string | number } | false;
+ gpsExif?: boolean;
+ annotate?: (number | string)[];
+ annotateExtra?: [number, string, string]; // fontSize, fontColor, backgroundColor
}
export default class StillCamera {
@@ -54,6 +80,58 @@ export default class StillCamera {
*/
'--nopreview',
+ /**
+ * RAW (Save Bayer Data)
+ * This option inserts the raw Bayer data from the camera in to the
+ * JPEG metadata.
+ */
+ ...(this.options.raw ? ['--raw'] : []),
+
+ /**
+ * JPEG Quality
+ * Quality 100 is almost completely uncompressed. 75 is a good allround value.
+ */
+ ...(this.options.quality ? ['--quality', this.options.quality.toString()] : []),
+
+ /**
+ * Burst
+ * This prevents the camera from returning to preview mode in between captures,
+ * meaning that captures can be taken closer together.
+ */
+ ...(this.options.burst ? ['--burst'] : []),
+
+ /**
+ * Thumbnail Settings (x:y:quality)
+ * Allows specification of the thumbnail image inserted in to the JPEG file.
+ * If not specified, defaults are a size of 64x48 at quality 35.
+ * `false` will remove the default thumbnail
+ */
+ ...(Array.isArray(this.options.thumbnail) || this.options.thumbnail === false
+ ? ['--thumb', !this.options.thumbnail ? 'none' : this.options.thumbnail.join(':')]
+ : []),
+
+ /**
+ * Exif information
+ * Allows the insertion of specific EXIF tags into the JPEG image.
+ * You can have up to 32 EXIF tag entries.
+ * Will overwrite any EXIF tag set automatically by the camera.
+ */
+ ...(this.options.exif
+ ? Object.keys(this.options.exif).flatMap(key => [
+ '--exif',
+ `${key}=${(this.options.exif as any)[key as keyof StillOptions['exif']]}`,
+ ])
+ : []),
+ // `false` will remove all the default EXIF information
+ ...(this.options.exif === false ? ['--exif', 'none'] : []),
+
+ /**
+ * GPS Exif
+ * Applies real-time EXIF information from any attached GPS dongle (using GSPD) to the image
+ * (requires libgps.so to be installed)
+ */
+ ...(this.options.gpsExif ? ['--gpsexif'] : []),
+
/**
* Output to stdout
*/
@@ -61,7 +139,7 @@ export default class StillCamera {
'-',
]);
} catch (err) {
- if (err.code === 'ENOENT') {
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(
"Could not take image with StillCamera. Are you running on a Raspberry Pi with 'raspistill' installed?",
);
diff --git a/src/lib/stream-camera.test.ts b/src/lib/stream-camera.test.ts
index 22f1b6c..3d96bef 100644
--- a/src/lib/stream-camera.test.ts
+++ b/src/lib/stream-camera.test.ts
@@ -1,6 +1,17 @@
+import * as fs from 'fs';
+import { performance } from 'perf_hooks';
+
import StreamCamera, { Codec } from './stream-camera';
+const TEST_IMAGES_DIR = 'test_images';
+
+if (!fs.existsSync(TEST_IMAGES_DIR)) {
+ fs.mkdirSync(TEST_IMAGES_DIR);
+}
+
test('Method takeImage() grabs JPEG from MJPEG stream', async () => {
+ const t0 = performance.now();
+
const streamCamera = new StreamCamera({
codec: Codec.MJPEG,
});
@@ -10,6 +21,10 @@ test('Method takeImage() grabs JPEG from MJPEG stream', async () => {
const jpegImage = await streamCamera.takeImage();
await streamCamera.stopCapture();
+ const t1 = performance.now();
+
+ const time = ((t1 - t0) / 1000).toFixed(2);
+ await fs.promises.writeFile(`test_images/streamCapture_(${time}-secs).jpeg`, jpegImage, 'binary');
expect(jpegImage.indexOf(StreamCamera.jpegSignature)).toBe(0);
});
diff --git a/src/lib/stream-camera.ts b/src/lib/stream-camera.ts
index 693ab3a..848e27c 100644
--- a/src/lib/stream-camera.ts
+++ b/src/lib/stream-camera.ts
@@ -1,7 +1,16 @@
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { EventEmitter } from 'events';
import * as stream from 'stream';
-import { AwbMode, ExposureMode, Flip, Rotation } from '..';
+import {
+ AwbMode,
+ DynamicRange,
+ ExposureMode,
+ FlickerMode,
+ Flip,
+ ImageEffectMode,
+ MeteringMode,
+ Rotation,
+} from '..';
import { getSharedArgs } from './shared-args';
export enum Codec {
@@ -38,8 +47,19 @@ export interface StreamOptions {
exposureCompensation?: number;
exposureMode?: ExposureMode;
awbMode?: AwbMode;
+ awbGains?: [number, number];
analogGain?: number;
digitalGain?: number;
+ imageEffectMode?: ImageEffectMode;
+ colorEffect?: [number, number]; // U,V
+ dynamicRange?: DynamicRange;
+ videoStabilization?: boolean;
+ statistics?: boolean;
+ meteringMode?: MeteringMode;
+ flickerMode?: FlickerMode;
+ roi?: [number, number, number, number]; // X, Y, W, H
+ annotate?: (number | string)[];
+ annotateExtra?: [number, string, string]; // fontSize, fontColor, backgroundColor
}
declare interface StreamCamera {
@@ -126,7 +146,22 @@ class StreamCamera extends EventEmitter {
* | 4 | 1640x1232 | 4:3 | 0.1-40fps | Full | 2x2 |
* | 5 | 1640x922 | 16:9 | 0.1-40fps | Full | 2x2 |
* | 6 | 1280x720 | 16:9 | 40-90fps | Partial | 2x2 |
- * | 7 | 640x480 | 4:3 | 40-90fps | Partial | 2x2 |
+ * | 7 | 640x480 | 4:3 | 40-200fps* | Partial | 2x2 |
+ *
+ * *For frame rates over 120fps, it is necessary to turn off automatic exposure and gain
+ * control using -ex off. Doing so should achieve the higher frame rates, but exposure
+ * time and gains will need to be set to fixed values supplied by the user.
+ *
+ *
+ * HQ Camera (IMX477):
+ *
+ * | Mode | Size | Aspect Ratio | Frame rates | FOV | Binning |
+ * |------|---------------------|--------------|-------------|---------|-------------|
+ * | 0 | automatic selection | | | | |
+ * | 1 | 2028x1080 | 169:90 | 0.1-50fps | Partial | 2x2 binned |
+ * | 2 | 2028x1520 | 4:3 | 0.1-50fps | Full | 2x2 binned |
+ * | 3 | 4056x3040 | 4:3 | 0.005-10fps | Full | None |
+ * | 4 | 1332x990 | 74:55 | 50.1-120fps | Partial | 2x2 binned |
*
*/
...(this.options.sensorMode ? ['--mode', this.options.sensorMode.toString()] : []),