Skip to content

Commit 8cdf789

Browse files
Always display camera preview with highest res possible (#685)
* Always display camera preview with highest res possible * Small tweaks
1 parent f6fe0b8 commit 8cdf789

File tree

11 files changed

+367
-162
lines changed

11 files changed

+367
-162
lines changed

apps/monk-test-app/src/views/TestView/TestView.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@
77
justify-content: center;
88
color: white;
99
}
10+
11+
.select-container {
12+
position: fixed;
13+
top: 50px;
14+
left: 50px;
15+
z-index: 9999;
16+
}
Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,39 @@
1-
import React, { useState } from 'react';
2-
import { PhotoCapture } from '@monkvision/inspection-capture-web';
3-
import { sights } from '@monkvision/sights';
1+
import { Camera, CameraResolution, MonkPicture, SimpleCameraHUD } from '@monkvision/camera-web';
42
import './TestView.css';
5-
6-
const captureSights = [
7-
sights['haccord-8YjMcu0D'],
8-
sights['haccord-DUPnw5jj'],
9-
sights['haccord-hsCc_Nct'],
10-
sights['haccord-GQcZz48C'],
11-
sights['haccord-QKfhXU7o'],
12-
sights['haccord-mdZ7optI'],
13-
sights['haccord-bSAv3Hrj'],
14-
sights['haccord-W-Bn3bU1'],
15-
sights['haccord-GdWvsqrm'],
16-
sights['haccord-ps7cWy6K'],
17-
sights['haccord-Jq65fyD4'],
18-
sights['haccord-OXYy5gET'],
19-
sights['haccord-5LlCuIfL'],
20-
sights['haccord-Gtt0JNQl'],
21-
sights['haccord-cXSAj2ez'],
22-
sights['haccord-KN23XXkX'],
23-
sights['haccord-Z84erkMb'],
24-
];
25-
26-
const inspectionId = 'b072ff42-6244-ef3b-b018-5d3d6562c1bd';
27-
28-
const apiConfig = {
29-
apiDomain: 'api.preview.monk.ai/v1',
30-
authToken:
31-
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjNLUnpaNm01WDFzOWFBZWRudnBrWSJ9.eyJpc3MiOiJodHRwczovL2lkcC5wcmV2aWV3Lm1vbmsuYWkvIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMDY5MzYxMTEwMDU4MDYxODA1NTYiLCJhdWQiOlsiaHR0cHM6Ly9hcGkubW9uay5haS92MS8iLCJodHRwczovL21vbmstcHJldmlldy5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNzA3NDcxNzU5LCJleHAiOjE3MDc0Nzg5NTksImF6cCI6InNvWjdQMmM2YjlJNWphclFvUnJoaDg3eDlUcE9TYUduIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsInBlcm1pc3Npb25zIjpbIm1vbmtfY29yZV9hcGk6Y29tcGxpYW5jZXMiLCJtb25rX2NvcmVfYXBpOmRhbWFnZV9kZXRlY3Rpb24iLCJtb25rX2NvcmVfYXBpOmRhc2hib2FyZF9vY3IiLCJtb25rX2NvcmVfYXBpOmltYWdlc19vY3IiLCJtb25rX2NvcmVfYXBpOmluc3BlY3Rpb25zOmNyZWF0ZSIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6ZGVsZXRlIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpkZWxldGVfYWxsIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpkZWxldGVfb3JnYW5pemF0aW9uIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpyZWFkIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpyZWFkX2FsbCIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6cmVhZF9vcmdhbml6YXRpb24iLCJtb25rX2NvcmVfYXBpOmluc3BlY3Rpb25zOnVwZGF0ZSIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6dXBkYXRlX2FsbCIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6dXBkYXRlX29yZ2FuaXphdGlvbiIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6d3JpdGUiLCJtb25rX2NvcmVfYXBpOmluc3BlY3Rpb25zOndyaXRlX2FsbCIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6d3JpdGVfb3JnYW5pemF0aW9uIiwibW9ua19jb3JlX2FwaTpyZXBhaXJfZXN0aW1hdGUiLCJtb25rX2NvcmVfYXBpOnVzZXJzOnJlYWQiLCJtb25rX2NvcmVfYXBpOnVzZXJzOnJlYWRfYWxsIiwibW9ua19jb3JlX2FwaTp1c2VyczpyZWFkX29yZ2FuaXphdGlvbiIsIm1vbmtfY29yZV9hcGk6dXNlcnM6dXBkYXRlIiwibW9ua19jb3JlX2FwaTp1c2Vyczp1cGRhdGVfYWxsIiwibW9ua19jb3JlX2FwaTp1c2Vyczp1cGRhdGVfb3JnYW5pemF0aW9uIiwibW9ua19jb3JlX2FwaTp1c2Vyczp3cml0ZSIsIm1vbmtfY29yZV9hcGk6dXNlcnM6d3JpdGVfYWxsIiwibW9ua19jb3JlX2FwaTp1c2Vyczp3cml0ZV9vcmdhbml6YXRpb24iLCJtb25rX2NvcmVfYXBpOndoZWVsX2FuYWx5c2lzIl19.m38G5NaCD_pcGptJdLPa_TVtD08yRY9qdp-5pCELd8Ekzma0kCWMctwHvlrv2OsjynlXyAutIK2uhDMrdLdnPk_6bU4rZheej2s0obXaXqZCUTbUOh8scL-81tbH_ZlKN3oSXfqUVMnwvpa1bnXZHmjeHi2e3bhvjxW-Jg5DrBB9gNfstxK0hugrlxtNL96y6ImEITOxOMEbURYGwOLQQtLkRqFo7AeZCu-_w6UbtFZfLJc5FlsWpTKy7I3_xMynzDGGaADRQeyazL_7DfemCb_VPXkR9aV67tF4pt6jevCetYI5CfN5X2JJrp7XNES_M2d7wGdJl5C6lbBPs3n3SA',
32-
};
3+
import { useState } from 'react';
334

345
export function TestView() {
35-
const [complete, setComplete] = useState(false);
6+
const [resolution, setResolution] = useState(CameraResolution.UHD_4K);
7+
8+
const handlePictureTaken = (picture: MonkPicture) => {
9+
const link = document.createElement('a');
10+
link.href = picture.uri;
11+
const now = new Date();
12+
link.download = `pic-${resolution}-${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}.jpg`;
13+
document.body.appendChild(link);
14+
link.click();
15+
document.body.removeChild(link);
16+
};
3617

3718
return (
3819
<div className='test-view-container'>
39-
{complete ? (
40-
'Inspection Complete!'
41-
) : (
42-
<PhotoCapture
43-
sights={captureSights}
44-
inspectionId={inspectionId}
45-
apiConfig={apiConfig}
46-
onComplete={() => setComplete(true)}
47-
onClose={() => console.log('coucou')}
48-
/>
49-
)}
20+
<Camera
21+
HUDComponent={SimpleCameraHUD}
22+
resolution={resolution}
23+
onPictureTaken={handlePictureTaken}
24+
/>
25+
<div className='select-container'>
26+
<select
27+
value={resolution}
28+
onChange={(e) => setResolution(e.target.value as CameraResolution)}
29+
>
30+
{Object.values(CameraResolution).map((res) => (
31+
<option key={res} value={res}>
32+
{res}
33+
</option>
34+
))}
35+
</select>
36+
</div>
5037
</div>
5138
);
5239
}

packages/public/camera-web/README.md

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,9 @@ function MyCameraPreview() {
3333
}
3434
```
3535

36-
## Camera constraints
37-
The resolution quality of the camera of the Camera video stream that is fetched from the user's device is configurable
38-
by passing it as a prop to the Camera component. Note that device selection (selecting which Camera will be used when
39-
the device has many available) is disabled for now. This is because this is instead handled automatically by the
40-
component in order to prevent unusable cameras (zoomed ones for instance) to be used.
41-
42-
Example of how to configure the resolution of the Camera :
36+
## Camera resolution
37+
The resolution quality of the pictures taken by the Camera component is configurable by passing it as a prop to the
38+
Camera component :
4339

4440
```tsx
4541
import { Camera, CameraResolution } from '@monkvision/camera-web';
@@ -49,20 +45,15 @@ function MyCameraPreview() {
4945
}
5046
```
5147

52-
For more details on the camera constraints options, see the *API* section below.
53-
5448
Notes :
55-
- When the camera constraints are updated, the video stream is automatically updated without asking the user for
56-
permissions another time
57-
- Only the resolutions available in the `CameraResolution` enum are allowed for better results with our AI models
58-
- The resolutions available in the `CameraResolution` enum are all in the 16:9 format
59-
- Device selection (selecting which Camera will be used when the device has many available) is disabled for now. This is
60-
because this is instead handled automatically by the component in order to prevent unusable cameras (zoomed ones for
61-
instance) to be used.
62-
- If no device meets the given requirements, the device with the closest match will be used :
63-
- If the needed resolution is too high, the highest resolution will be used : this means that asking for the
64-
`CameraResolution.UHD_4K` resolution is a good way to get the highest resolution possible
65-
- If the needed resolution is too low the browser will crop and scale the camera's feed to meet the requirements
49+
- This option does not affect the resolution of the Camera preview : the preview will always use the highest
50+
resolution available on the current device.
51+
- If the specified resolution is not equal to the one used by the device's native camera, the pictures taken will be
52+
scaled to fit the requirements.
53+
- The resolutions available in the `CameraResolution` enum are all in the 16:9 format.
54+
- If the aspect ratio of the specified resolution differs from the one of the device's camera, pictures taken will
55+
always have the same aspect ratio as the native camera one, and will be scaled in a way to make sure that neither the
56+
width, nor the height of the output picture will exceed the dimensions of the specified resolution.
6657

6758
## Compression options
6859
When pictures are taken by the camera, they are compressed and encoded. The compression format and quality can be
@@ -169,7 +160,7 @@ Main component exported by this package, displays a Camera preview and the given
169160
### Props
170161
| Prop | Type | Description | Required | Default Value |
171162
|----------------|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------|--------------------------------|
172-
| resolution | CameraResolution | Resolution of the camera to use. | | `CameraResolution.UHD_4K` |
163+
| resolution | CameraResolution | Resolution of the pictures taken by the camera. This does not affect the resolution of the Camera preview. | | `CameraResolution.UHD_4K` |
173164
| format | CompressionFormat | The compression format used to compress images taken by the camera. | | `CompressionFormat.JPEG` |
174165
| quality | number | The image quality when using a compression format that supports lossy compression. From 0 (lowest quality) to 1 (best quality). | | `0.8` |
175166
| HUDComponent | CameraHUDComponent<T> | The camera HUD component to display on top of the camera preview. | | |

packages/public/camera-web/src/Camera/Camera.tsx

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { useMemo } from 'react';
22
import { AllOrNone, RequiredKeys } from '@monkvision/types';
33
import {
4-
CameraConfig,
54
CameraFacingMode,
65
CameraResolution,
76
CompressionFormat,
@@ -47,10 +46,31 @@ export type HUDConfigProps<T extends object> = RequiredKeys<T> extends never
4746
/**
4847
* Props given to the Camera component. The generic T type corresponds to the prop types of the HUD.
4948
*/
50-
export type CameraProps<T extends object> = Partial<Pick<CameraConfig, 'resolution'>> &
51-
Partial<CompressionOptions> &
49+
export type CameraProps<T extends object> = Partial<CompressionOptions> &
5250
CameraEventHandlers &
5351
HUDConfigProps<T> & {
52+
/**
53+
* This option specifies the resolution of the pictures taken by the Camera. This option does not affect the
54+
* resolution of the Camera preview (it will always be the highest resolution possible). If the specified resolution
55+
* is not equal to the one used by the device's native camera, the pictures taken will be scaled to fit the
56+
* requirements. Note that if the aspect ratio of the specified resolution differs from the one of the device's
57+
* camera, pictures taken will always have the same aspect ratio as the native camera one, and will be scaled in a way
58+
* to make sure that neither the width, nor the height of the output picture will exceed the dimensions of the
59+
* specified resolution.
60+
*
61+
* Note: If the specified resolution is higher than the best resolution available on the current device, output
62+
* pictures will only be scaled up to the specified resolution if the `allowImageUpscaling` property is set to `true`.
63+
*
64+
* @default `CameraResolution.UHD_4K`
65+
*/
66+
resolution?: CameraResolution;
67+
/**
68+
* When the native resolution of the device Camera is smaller than the resolution asked in the `resolution` prop,
69+
* resulting pictures will only be scaled up if this property is set to `true`.
70+
*
71+
* @default `false`
72+
*/
73+
allowImageUpscaling?: boolean;
5474
/**
5575
* Additional monitoring config that can be provided to the Camera component.
5676
*/
@@ -71,20 +91,32 @@ export function Camera<T extends object>({
7191
resolution = CameraResolution.UHD_4K,
7292
format = CompressionFormat.JPEG,
7393
quality = 0.8,
94+
allowImageUpscaling = false,
7495
HUDComponent,
7596
hudProps,
7697
monitoring,
7798
onPictureTaken,
7899
}: CameraProps<T>) {
79100
const {
80101
ref: videoRef,
81-
dimensions,
102+
dimensions: streamDimensions,
82103
error,
83104
retry,
84105
isLoading: isPreviewLoading,
85-
} = useCameraPreview({ resolution, facingMode: CameraFacingMode.ENVIRONMENT });
86-
const { ref: canvasRef } = useCameraCanvas({ dimensions });
87-
const { takeScreenshot } = useCameraScreenshot({ videoRef, canvasRef, dimensions });
106+
} = useCameraPreview({
107+
resolution: CameraResolution.UHD_4K,
108+
facingMode: CameraFacingMode.ENVIRONMENT,
109+
});
110+
const { ref: canvasRef, dimensions: canvasDimensions } = useCameraCanvas({
111+
resolution,
112+
streamDimensions,
113+
allowImageUpscaling,
114+
});
115+
const { takeScreenshot } = useCameraScreenshot({
116+
videoRef,
117+
canvasRef,
118+
dimensions: canvasDimensions,
119+
});
88120
const { compress } = useCompression({ canvasRef, options: { format, quality } });
89121
const { takePicture, isLoading: isTakePictureLoading } = useTakePicture({
90122
compress,
@@ -112,7 +144,7 @@ export function Camera<T extends object>({
112144

113145
return HUDComponent ? (
114146
<HUDComponent
115-
handle={{ takePicture, error, retry, isLoading, dimensions }}
147+
handle={{ takePicture, error, retry, isLoading, dimensions: streamDimensions }}
116148
cameraPreview={cameraPreview}
117149
{...((hudProps ?? {}) as T)}
118150
/>
Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
import { RefObject, useEffect, useRef } from 'react';
1+
import { RefObject, useEffect, useMemo, useRef } from 'react';
22
import { PixelDimensions } from '@monkvision/types';
3+
import { CameraResolution, getResolutionDimensions } from './utils';
34

45
/**
56
* Object used to configure the camera canvas.
67
*/
78
export interface CameraCanvasConfig {
9+
/**
10+
* The resolution of the pictures taken asked by the user of the Camera component.
11+
*/
12+
resolution: CameraResolution;
13+
/**
14+
* Boolean indicating if the Camera component should allow image upscaling when the asked resolution is bigger than
15+
* the one of the device Camera.
16+
*/
17+
allowImageUpscaling: boolean;
818
/**
919
* The dimensions of the video stream.
1020
*/
11-
dimensions: PixelDimensions | null;
21+
streamDimensions: PixelDimensions | null;
1222
}
1323

1424
/**
@@ -19,13 +29,65 @@ export interface CameraCanvasHandle {
1929
* The ref to the canvas element. Forward this ref to the <canvas> tag to set it up.
2030
*/
2131
ref: RefObject<HTMLCanvasElement>;
32+
/**
33+
* The dimensions of the canvas.
34+
*/
35+
dimensions: PixelDimensions | null;
36+
}
37+
38+
/**
39+
* This function is used to calculate the dimensions of the canvas that will be used to draw the image, thus also
40+
* calculating the output dimensions of the image itself, respecting the following logic :
41+
* - If the aspect ratio of the stream and constraints are the same, we simply scale the stream to make it fit the
42+
* constraints. Note that if `allowImageUpscaling` is `false`, and the stream is smaller than the constraints, we don't
43+
* scale "up" the stream image, and we simply return the stream dimensions.
44+
* - If the aspect ratio of the stream is different from the one specified in the constraints, the logic is the same,
45+
* but the output aspect ratio will be the same one as the stream. The stream dimensions will simply be scaled
46+
* following the same logic as the previous point, making sure that neither the width nor the height of the canvas will
47+
* exceed the ones described by the constraints.
48+
*/
49+
function getCanvasDimensions({
50+
resolution,
51+
streamDimensions,
52+
allowImageUpscaling,
53+
}: CameraCanvasConfig): PixelDimensions | null {
54+
if (!streamDimensions) {
55+
return null;
56+
}
57+
const isPortrait = streamDimensions.width < streamDimensions.height;
58+
const constraintsDimensions = getResolutionDimensions(resolution, isPortrait);
59+
const streamRatio = streamDimensions.width / streamDimensions.height;
60+
61+
if (
62+
constraintsDimensions.width > streamDimensions.width &&
63+
constraintsDimensions.height > streamDimensions.height &&
64+
!allowImageUpscaling
65+
) {
66+
return {
67+
width: streamDimensions.width,
68+
height: streamDimensions.height,
69+
};
70+
}
71+
const fitToHeight = constraintsDimensions.width / streamRatio > constraintsDimensions.height;
72+
return {
73+
width: fitToHeight ? constraintsDimensions.height * streamRatio : constraintsDimensions.width,
74+
height: fitToHeight ? constraintsDimensions.height : constraintsDimensions.width / streamRatio,
75+
};
2276
}
2377

2478
/**
2579
* Custom hook used to manage the camera <canvas> element used to take video screenshots and encode images.
2680
*/
27-
export function useCameraCanvas({ dimensions }: CameraCanvasConfig): CameraCanvasHandle {
81+
export function useCameraCanvas({
82+
resolution,
83+
streamDimensions,
84+
allowImageUpscaling,
85+
}: CameraCanvasConfig): CameraCanvasHandle {
2886
const ref = useRef<HTMLCanvasElement>(null);
87+
const dimensions = useMemo(
88+
() => getCanvasDimensions({ resolution, streamDimensions, allowImageUpscaling }),
89+
[resolution, streamDimensions],
90+
);
2991

3092
useEffect(() => {
3193
if (dimensions && ref.current) {
@@ -34,5 +96,5 @@ export function useCameraCanvas({ dimensions }: CameraCanvasConfig): CameraCanva
3496
}
3597
}, [dimensions]);
3698

37-
return { ref };
99+
return { ref, dimensions };
38100
}

packages/public/camera-web/src/Camera/hooks/utils/getMediaContraints.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { PixelDimensions } from '@monkvision/types';
2+
13
/**
24
* Enumeration of the facing modes for the camera constraints.
35
*/
@@ -50,8 +52,8 @@ export enum CameraResolution {
5052
UHD_4K = '4K',
5153
}
5254

53-
const CAMERA_RESOLUTION_SIZES: {
54-
[key in CameraResolution]: { width: number; height: number };
55+
const CAMERA_RESOLUTION_DIMENSIONS: {
56+
[key in CameraResolution]: PixelDimensions;
5557
} = {
5658
[CameraResolution.QNHD_180P]: { width: 320, height: 180 },
5759
[CameraResolution.NHD_360P]: { width: 640, height: 360 },
@@ -68,8 +70,6 @@ export interface CameraConfig {
6870
/**
6971
* Specifies which camera to use if the devices has a front and a rear camera. If the device does not have a camera
7072
* meeting the requirements, the closest one will be used.
71-
*
72-
* @default `CameraFacingMode.ENVIRONMENT`
7373
*/
7474
facingMode: CameraFacingMode;
7575
/**
@@ -78,12 +78,24 @@ export interface CameraConfig {
7878
* - The Monk Camera package will always try to fetch a stream with a 16:9 resolution format.
7979
* - The implementation of the algorithm used to choose the closest camera can differ between browsers, and if the
8080
* exact requirements can't be met, the resulting stream's quality can differ between browsers.
81-
*
82-
* @default `CameraResolution.UHD_4K`
8381
*/
8482
resolution: CameraResolution;
8583
}
8684

85+
/**
86+
* Utility function that returns the dimensions in pixels of the given `CameraResolution`.
87+
*/
88+
export function getResolutionDimensions(
89+
resolution: CameraResolution,
90+
isPortrait = false,
91+
): PixelDimensions {
92+
const dimensions = CAMERA_RESOLUTION_DIMENSIONS[resolution];
93+
return {
94+
width: isPortrait ? dimensions.height : dimensions.width,
95+
height: isPortrait ? dimensions.width : dimensions.height,
96+
};
97+
}
98+
8799
/**
88100
* This function is used by the Monk Camera package in order to add a layer of abstraction to the media constraints
89101
* passed to the `useUserMedia` hook. It takes an optional `CameraOptions` parameter and creates a
@@ -94,7 +106,7 @@ export interface CameraConfig {
94106
* @see useUserMedia
95107
*/
96108
export function getMediaConstraints(config: CameraConfig): MediaStreamConstraints {
97-
const { width, height } = CAMERA_RESOLUTION_SIZES[config.resolution];
109+
const { width, height } = getResolutionDimensions(config.resolution);
98110

99111
const video: MediaTrackConstraints = {
100112
width: { ideal: width },

0 commit comments

Comments
 (0)