Skip to content

Commit

Permalink
fix: more get it working
Browse files Browse the repository at this point in the history
  • Loading branch information
Charles Parker authored and Charles Parker committed Apr 28, 2024
1 parent d3d9acf commit ccdf2b3
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 71 deletions.
2 changes: 1 addition & 1 deletion examples/objectdetection/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1435,4 +1435,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 106c37d775a4ea3a9fa9744362f0af5ba16aac0e

COCOAPODS: 1.15.2
COCOAPODS: 1.14.3
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,11 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
);
OTHER_LDFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
Expand Down Expand Up @@ -654,7 +658,11 @@
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
);
OTHER_LDFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
Expand Down
44 changes: 21 additions & 23 deletions examples/objectdetection/src/CameraStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,7 @@ import {
} from "@shopify/react-native-skia";
import * as React from "react";

import {
Platform,
Pressable,
StyleSheet,
Text,
View,
useWindowDimensions,
} from "react-native";
import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
import {
Delegate,
MediapipeCamera,
Expand All @@ -29,6 +22,7 @@ import {
} from "react-native-vision-camera";
import type { RootTabParamList } from "./navigation";
import type { BottomTabScreenProps } from "@react-navigation/bottom-tabs";
import { frameRectToView, ltrbToXywh } from "../../../src/shared/convert";

interface Detection {
label: string;
Expand All @@ -41,7 +35,6 @@ interface Detection {
type Props = BottomTabScreenProps<RootTabParamList, "CameraStream">;

export const CameraStream: React.FC<Props> = () => {
const { width, height } = useWindowDimensions();
const camPerm = useCameraPermission();
const micPerm = useMicrophonePermission();
const [permsGranted, setPermsGranted] = React.useState<{
Expand Down Expand Up @@ -75,25 +68,28 @@ export const CameraStream: React.FC<Props> = () => {
);
};

const frameProcessor = useObjectDetection(
(results) => {
console.log(results);
const objectDetection = useObjectDetection(
(results, viewSize) => {
const firstResult = results.results[0];
const detections = firstResult?.detections ?? [];
const frameSize = {
width: results.inputImageWidth,
height: results.inputImageHeight,
};
setObjectFrames(
detections.map((detection) => {
const { x, y, width, height } = frameRectToView(
ltrbToXywh(detection.boundingBox),
frameSize,
viewSize,
"cover"
);
return {
label: detection.categories[0]?.categoryName ?? "unknown",
x: (detection.boundingBox.left / results.inputImageWidth) * width,
y: (detection.boundingBox.top / results.inputImageHeight) * height,
width:
((detection.boundingBox.right - detection.boundingBox.left) /
results.inputImageWidth) *
width,
height:
((detection.boundingBox.bottom - detection.boundingBox.top) /
results.inputImageHeight) *
height,
x,
y,
width,
height,
};
})
);
Expand All @@ -111,8 +107,9 @@ export const CameraStream: React.FC<Props> = () => {
<View style={styles.container}>
<MediapipeCamera
style={styles.box}
processor={frameProcessor}
solution={objectDetection}
activeCamera={active}
resizeMode="cover" // must agree with frameRectToView above
/>
<Canvas style={styles.box}>
{objectFrames.map((frame, index) => (
Expand Down Expand Up @@ -172,6 +169,7 @@ const ObjectFrame: React.FC<{ frame: Detection; index: number }> = ({

const styles = StyleSheet.create({
container: {
backgroundColor: "red",
flex: 1,
alignItems: "center",
justifyContent: "center",
Expand Down
36 changes: 2 additions & 34 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,3 @@
import React from "react";
import { type ViewStyle, Text, Platform } from "react-native";
import {
Camera,
useCameraDevice,
type CameraPosition,
type FrameProcessor,
} from "react-native-vision-camera";

export type MediapipeCameraProps = {
style: ViewStyle;
processor: FrameProcessor;
activeCamera: CameraPosition;
};

export const MediapipeCamera: React.FC<MediapipeCameraProps> = ({
style,
processor,
activeCamera,
}) => {
const device = useCameraDevice(activeCamera);
return device !== undefined ? (
<Camera
style={style}
device={device}
pixelFormat={Platform.select({ ios: "rgb", android: "yuv" })}
isActive={true}
frameProcessor={processor}
/>
) : (
<Text>no device</Text>
);
};

export * from "./objectDetection";
export * from "./shared/mediapipeCamera";
export * from "./shared/convert";
51 changes: 40 additions & 11 deletions src/objectDetection/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React from "react";
import { NativeEventEmitter, NativeModules } from "react-native";
import {
NativeEventEmitter,
NativeModules,
type LayoutChangeEvent,
} from "react-native";
import {
VisionCameraProxy,
useFrameProcessor,
} from "react-native-vision-camera";
import type { MediaPipeSolution } from "../shared/types";
import type { Dims } from "../shared/convert";

const { ObjectDetection } = NativeModules;
const eventEmitter = new NativeEventEmitter(ObjectDetection);
Expand Down Expand Up @@ -96,8 +102,9 @@ export interface ObjectDetectionOptions {
resize: { scale: number; aspect: "preserve" | "default" | number };
}
export interface ObjectDetectionCallbacks {
onResults: (result: ResultBundleMap) => void;
onResults: (result: ResultBundleMap, viewSize: Dims) => void;
onError: (error: ObjectDetectionError) => void;
viewSize: Dims;
}

// TODO setup the general event callbacks
Expand All @@ -107,7 +114,7 @@ eventEmitter.addListener(
(args: { handle: number } & ResultBundleMap) => {
const callbacks = detectorMap.get(args.handle);
if (callbacks) {
callbacks.onResults(args);
callbacks.onResults(args, callbacks.viewSize);
}
}
);
Expand All @@ -127,17 +134,36 @@ export function useObjectDetection(
runningMode: RunningMode,
model: string,
options?: Partial<ObjectDetectionOptions>
) {
): MediaPipeSolution {
const [detectorHandle, setDetectorHandle] = React.useState<
number | undefined
>();

const [cameraViewDimensions, setCameraViewDimensions] = React.useState<{
width: number;
height: number;
}>({ width: 1, height: 1 });

const cameraViewLayoutChangeHandler = React.useCallback(
(event: LayoutChangeEvent) => {
setCameraViewDimensions({
height: event.nativeEvent.layout.height,
width: event.nativeEvent.layout.width,
});
},
[]
);

// Remember the latest callback if it changes.
React.useLayoutEffect(() => {
if (detectorHandle !== undefined) {
detectorMap.set(detectorHandle, { onResults, onError });
detectorMap.set(detectorHandle, {
onResults,
onError,
viewSize: cameraViewDimensions,
});
}
}, [onResults, onError, detectorHandle]);
}, [onResults, onError, detectorHandle, cameraViewDimensions]);

React.useEffect(() => {
let newHandle: number | undefined;
Expand Down Expand Up @@ -182,15 +208,18 @@ export function useObjectDetection(

plugin?.call(frame, {
detectorHandle,
scale: {
width: frame.width * 0.5,
height: frame.height * 0.5,
},
pixelFormat: "rgb",
dataType: "uint8",
});
},
[detectorHandle]
);
return frameProcessor;
return React.useMemo(
(): MediaPipeSolution => ({
cameraViewLayoutChangeHandler,
cameraViewDimensions,
frameProcessor,
}),
[cameraViewDimensions, cameraViewLayoutChangeHandler, frameProcessor]
);
}
128 changes: 128 additions & 0 deletions src/shared/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
export type Dims = { width: number; height: number };
export type Point = { x: number; y: number };
export type RectXYWH = { x: number; y: number; width: number; height: number };
export type RectLTRB = {
left: number;
top: number;
right: number;
bottom: number;
};
export type ResizeMode = "cover" | "contain";

// both cover and contain preserve aspect ratio. Cover will crop the image to fill the view, contain will show the whole image and add padding.
// for cover, if the aspect ratio x/y of the frame is greater than
export function framePointToView(
point: Point,
frameDims: Dims,
viewDims: Dims,
mode: ResizeMode
): Point {
const frameRatio = frameDims.width / frameDims.height;
const viewRatio = viewDims.width / viewDims.height;
let scale = 1;
let xoffset = 0;
let yoffset = 0;
if (mode === "contain") {
// contain means that the frame rect will be smaller than the view rect,
// if the w/h ratio of the frame is greater than the w/h ratio of the view,
// then equal in the x dimension, smaller in the y dimension
// else the other way around
if (frameRatio > viewRatio) {
scale = viewDims.width / frameDims.width;
xoffset = 0;
yoffset = (viewDims.height - frameDims.height * scale) / 2;
} else {
scale = viewDims.height / frameDims.height;
xoffset = (viewDims.width - frameDims.width * scale) / 2;
yoffset = 0;
}
} else {
if (frameRatio > viewRatio) {
scale = viewDims.height / frameDims.height;
xoffset = (viewDims.width - frameDims.width * scale) / 2;
yoffset = 0;
} else {
scale = viewDims.width / frameDims.width;
xoffset = 0;
yoffset = (viewDims.height - frameDims.height * scale) / 2;
}
}
return {
x: point.x * scale + xoffset,
y: point.y * scale + yoffset,
};
}

function frameRectLTRBToView(
rect: RectLTRB,
frameDims: Dims,
viewDims: Dims,
mode: ResizeMode
): RectLTRB {
const lt = framePointToView(
{ x: rect.left, y: rect.top },
frameDims,
viewDims,
mode
);
const rb = framePointToView(
{ x: rect.right, y: rect.bottom },
frameDims,
viewDims,
mode
);
return { left: lt.x, top: lt.y, right: rb.x, bottom: rb.y };
}

function frameRectXYWHToView(
rect: RectXYWH,
frameDims: Dims,
viewDims: Dims,
mode: ResizeMode
): RectXYWH {
const lt = framePointToView(
{ x: rect.x, y: rect.y },
frameDims,
viewDims,
mode
);
const rb = framePointToView(
{ x: rect.x + rect.width, y: rect.y + rect.height },
frameDims,
viewDims,
mode
);
return { x: lt.x, y: lt.y, width: rb.x - lt.x, height: rb.y - lt.y };
}

function isRectLTRB(rect: unknown): rect is RectLTRB {
return (
typeof rect === "object" &&
"left" in (rect as object) &&
"top" in (rect as object) &&
"right" in (rect as object) &&
"bottom" in (rect as object)
);
}

export function frameRectToView<TRect extends RectLTRB | RectXYWH>(
rect: TRect,
frameDims: Dims,
viewDims: Dims,
mode: ResizeMode
): TRect {
if (isRectLTRB(rect)) {
return frameRectLTRBToView(rect, frameDims, viewDims, mode) as TRect;
} else {
return frameRectXYWHToView(rect, frameDims, viewDims, mode) as TRect;
}
}

export function ltrbToXywh(rect: RectLTRB): RectXYWH {
return {
x: rect.left,
y: rect.top,
width: rect.right - rect.left,
height: rect.bottom - rect.top,
};
}
Loading

0 comments on commit ccdf2b3

Please sign in to comment.