You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Expo React Native应用换脸时调用ImageManipulator崩溃求助

问题排查与修复

你的代码在调用ImageManipulator.manipulateAsync执行裁剪时崩溃,核心原因及修复方案如下:

崩溃核心原因

  1. 坐标系统不匹配:通过Camera组件获取的人脸关键点坐标基于Camera预览视图尺寸,但你直接用该坐标乘以选中图片的宽高,两者尺寸差异导致裁剪区域严重越界。
  2. 人脸数据解构错误:错误将faces数组的人脸对象直接当作关键点集合使用,导致后续获取的坐标值无效。
  3. 裁剪参数无边界校验:计算出的裁剪起始坐标可能为负数、宽高可能为0,这些无效参数直接触发ImageManipulator崩溃。

修复后的完整代码

import { Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import React, { useCallback, useEffect, useRef } from 'react'
import { Camera } from "expo-camera";
import * as FaceDetector from 'expo-face-detector';
import * as ImageManipulator from 'expo-image-manipulator';
import useFaceContext from '../context/FaceProvider';

const windowWidth = Dimensions.get("window").width;
const windowHeight = Dimensions.get("window").height;
const CameraComponent = () => {

    const cameraRef = useRef(null);
    const {image, setFaces, faces, setSwappedImage, swappedImage} = useFaceContext();

    useEffect(() => {
      (async () => {
        const { status } = await Camera.requestCameraPermissionsAsync();
        if (status !== "granted") {
          alert("Camera permission not granted");
        }
      })();
    }, []);

    const handleFacesDetected = useCallback(({ faces }) => {
      setFaces(faces);
    }, []);

    const swapFaces = async () => {
        if (faces.length === 0) {
            alert('No face detected');
            return;
        }

        if (image === null) {
            alert('No image selected');
            return;
        }

        // 修复:正确解构人脸对象与关键点集合
        const [face] = faces;
        const faceLandmarks = face.landmarks;

        try {
            // 先获取原始图片尺寸,避免翻转操作干扰尺寸计算
            const imageInfo = await ImageManipulator.manipulateAsync(
                image,
                [],
                { format: 'png', compress: 1, base64: false }
            );
            const { width: imageWidth, height: imageHeight } = imageInfo;

            // 执行图片翻转
            const manipResult = await ImageManipulator.manipulateAsync(
                image,
                [{ flip: ImageManipulator.FlipType.Horizontal }],
                { format: 'png' }
            );
            const { uri: flippedImageUri } = manipResult;

            const leftEye = faceLandmarks["LEFT_EYE"];
            const rightEye = faceLandmarks["RIGHT_EYE"];
            const nose = faceLandmarks["NOSE_BASE"];
            const mouth = faceLandmarks["LEFT_MOUTH"];

            if (!leftEye || !rightEye || !nose || !mouth) {
                alert('Cannot detect facial features');
                return;
            }

            // 修复:计算Camera预览与目标图片的尺寸比例,转换坐标系统
            const cameraPreviewWidth = windowWidth;
            const cameraPreviewHeight = windowHeight * 0.5; // 对应Camera组件设置的高度比例
            const scaleX = imageWidth / cameraPreviewWidth;
            const scaleY = imageHeight / cameraPreviewHeight;

            // 将Camera坐标转换为目标图片的真实坐标
            const leftEyeX = leftEye.x * scaleX;
            const leftEyeY = leftEye.y * scaleY;
            const rightEyeX = rightEye.x * scaleX;
            const rightEyeY = rightEye.y * scaleY;
            const noseX = nose.x * scaleX;
            const noseY = nose.y * scaleY;
            const mouthX = mouth.x * scaleX;
            const mouthY = mouth.y * scaleY;

            const offsetX = 0.25;
            const offsetY = 0.15;

            let swappedFaceWidth = Math.abs(rightEyeX - leftEyeX);
            let swappedFaceHeight = Math.abs(mouthY - noseY);

            // 修复:限制宽高最小值,避免无效尺寸
            swappedFaceWidth = Math.max(swappedFaceWidth, 50);
            swappedFaceHeight = Math.max(swappedFaceHeight, 50);

            let swappedFaceX = leftEyeX - offsetX * swappedFaceWidth;
            let swappedFaceY = noseY - offsetY * swappedFaceHeight;

            // 修复:确保裁剪区域不超出图片边界
            swappedFaceX = Math.max(0, swappedFaceX);
            swappedFaceY = Math.max(0, swappedFaceY);
            swappedFaceX = Math.min(swappedFaceX, imageWidth - swappedFaceWidth);
            swappedFaceY = Math.min(swappedFaceY, imageHeight - swappedFaceHeight);

            // 最终校验裁剪区域有效性
            if (swappedFaceWidth <=0 || swappedFaceHeight <=0 || swappedFaceX <0 || swappedFaceY <0 || 
                swappedFaceX + swappedFaceWidth > imageWidth || swappedFaceY + swappedFaceHeight > imageHeight) {
                alert('Invalid crop region');
                return;
            }

            const result = await ImageManipulator.manipulateAsync(
                flippedImageUri,
                [
                    {
                        crop: {
                            originX: swappedFaceX,
                            originY: swappedFaceY,
                            width: swappedFaceWidth,
                            height: swappedFaceHeight,
                        },
                    },
                ],
                { format: 'png' }
            );

            setSwappedImage(result.uri);
        } catch (error) {
            console.error('Face swap error:', error);
            alert('Error occurred during face swapping');
        }
    };

    return <View style={styles.container}>
        <Camera style={styles.camera} type={Camera.Constants.Type.front} ref={cameraRef} onFacesDetected={handleFacesDetected} faceDetectorSettings={{ mode: FaceDetector.FaceDetectorMode.accurate, detectLandmarks: FaceDetector.FaceDetectorLandmarks.all, runClassifications: FaceDetector.FaceDetectorClassifications.all }}>
            {faces.map((face, index) =>
                <View
                    key={index}
                    style={[
                        styles.faceOutline,
                        {
                            top: face.bounds.origin.y,
                            left: face.bounds.origin.x,
                            width: face.bounds.size.width,
                            height: face.bounds.size.height
                        }
                    ]}
                />
            )}
        </Camera>

        <View style={{
            flexDirection: "row",
            alignItems: "center",
            gap: 20
        }}>
            {image && <Image source={{ uri: image }} style={styles.image} />}
            {swappedImage && <Image source={{ uri: swappedImage }} style={styles.image} />}
        </View>

        <TouchableOpacity style={styles.button} onPress={swapFaces}>
            <Text style={styles.buttonText}>Swap Faces</Text>
        </TouchableOpacity>
    </View>;
}

export default CameraComponent

const styles = StyleSheet.create({
    container: {
        width: windowWidth,
        height: windowHeight,
        justifyContent: 'center',
        alignItems: 'center',
    },
    camera: {
        flex: 1,
        width: "100%",
        height: "50%",
    },
    faceOutline: {
        position: 'absolute',
        borderWidth: 2,
        borderColor: 'red',
        borderRadius: 5,
        opacity: 0.7,
    },
    image: {
        width: 100,
        height: 100,
        resizeMode: 'contain',
        marginVertical: 10,
    },
    button: {
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'black',
        borderRadius: 5,
        padding: 10,
        marginVertical: 10,
    },
    buttonText: {
        color: 'white',
        fontSize: 16,
        marginLeft: 5,
    },
    buttonIcon: {
        width: 20,
        height: 20,
        tintColor: 'white',
    },
});

关键修复点说明

  1. 坐标系统对齐:通过尺寸比例转换,将Camera预览的人脸坐标映射到目标图片的真实坐标,彻底解决裁剪区域越界问题。
  2. 数据结构修正:从face.landmarks中正确提取关键点数据,确保坐标值有效。
  3. 参数边界校验:
    • 强制限制裁剪宽高最小值,避免0或无效尺寸
    • 将裁剪起始坐标约束在图片范围内
    • 校验裁剪区域右下角不超出图片边界
  4. 分步处理图片:先获取原始图片尺寸,再执行翻转操作,避免翻转后的尺寸干扰坐标计算。

内容的提问来源于stack exchange,提问作者dasimems

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 09:47:01