Crear un diccionario contextualizado (móvil) con Machine Learning

24 de octubre de 2022

Dictionary by Joshua Hoehne
Photo by Joshua Hoehne on Unsplash

Digamos que estamos aprendiendo un idioma nuevo. Sacamos una foto de algún objeto y, a través de la foto, un sistema nos dice que objeto es en nuestro idioma nativo y la traducción de ese objeto en el idioma que estamos estudiando.

Para eso necesitaríamos algunas cosas:

  • Primero, una aplicación móvil. Para sacar la fotografía.
  • Segundo, una interacción con un sistema de Machine Learning, para la detección de objetos.
  • Tercero, una implementación con un servicio de traducción, para la traducción de ese objeto descubierto al idioma que estamos aprendiendo.

El resultado final sería algo como lo siguiente:

#.Configurando la aplicación móvil

Para esto, utilizaremos Expo.

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

Adicionalmente vamos a necesitar algunos paquetes, paso a listarlos desde la configuración del archivo package.json:

// ...
"@tensorflow-models/coco-ssd": "^2.1.0",
"@tensorflow/tfjs": "^3.3.0",
"@tensorflow/tfjs-react-native": "^0.6.0",
// ...
"expo-camera": "^12.1.2",
// ...
"translate-google-api": "^1.0.4"

#.Configurando el sistema de Machine Learning (TensorFlow)

Vamos a utilizar Tensor Flow. TensorFlow es:

TensorFlow is a free and open-source software library for machine learning and artificial intelligence. It can be used across a range of tasks but has a particular focus on training and inference of deep neural networks.

Detectar objetos es una de las funcionalidades principales de la implementación de machine learning a través de computer vision. Gracias a TensorFlow, podemos fácilmente utilizar el API disponible para crear y construir modelos de detección de objetos.

En nuestro caso, podemos utilizar modelos disponibles. Uno de esos modelos es CoCo-ssd o Common Objects in Context, donde SSD significa Single Shot MultiBox Detection.

En el paso anterior, ya importamos la dependencia tanto a TensorFlow como coco-ssd en nuestro proyecto, pero debemos conectarlo a alguna pantalla dentro de nuestra aplicación.

Es por ello que haremos una pantalla en nuestro proyecto, que es un componente de React.

// TabOneScreen.tsx

import React, { useEffect, useRef, useState } from "react";

export default function TabOneScree() {
}

Y procedemos a integrarla con algunas de las dependencias que ya importamos en nuestro paquete. De igual manera, requerimos acceso a la cámara y los permisos para acceder a ella.

// ...
const [isTfReady, setIsTfReady] = useState(false);
const [isModelReady, setIsModelReady] = useState(false);
const model = useRef(null);

useEffect(() => {
  const initializeTensorFlowAsync = async () => {
    await tf.setBackend('cpu');
    await tf.ready();
    setIsTfReady(true);
  };

  const initializeCocoModelAsync = async () => {
    model.current = await cocossd.load();
    setIsModelReady(true);
  };

  const getCameraPermissionsAsync = async () => {
    const {
      status:cameraPermissions,
    } = await ImagePicker.requestCameraPermissionsAsync();

    const {
      status:mediaPermission,
    } = await ImagePicker.requestMediaLibraryPermissionsAsync();

    if (cameraPermissions !== "granted" || mediaPermission !== "granted") {
      Alert.alert("Sorry, we need camera permissions to make this work!");
    }
  };

  initializeTensorFlowAsync();
  initializeCocoModelAsync();
  getCameraPermissionsAsync();
}, []);
// ...

En nuestro componente visual, que podemos separa a otro componente de React, vamos a mostrar el estado de la inicialización tanto de TensorFlow como del modelo:

// ...
<View style={styles.loadingContainer}>
  <View style={styles.loadingTfContainer}>
    <Text style={styles.text}>TensorFlow.js: </Text>
    {isTfReady ? (
      <Text style={styles.text}></Text>
    ) : (
      <ActivityIndicator size="small" color="#ffffff" />
    )}
  </View>

  <View style={styles.loadingModelContainer}>
    <Text style={styles.text}>Model (COCO-SSD): </Text>
    {isModelReady ? (
      <Text style={styles.text}></Text>
    ) : (
      <ActivityIndicator size="small" color="#ffffff" />
    )}
  </View>
</View>
// ...

#.Iniciando el proceso de toma de la fotografía

Para tomar la fotografía, podemos utilizar Expo Camera, pero queremos asegurarnos que tenemos acceso a la cámara (ver paso anterior) y también que tanto el modelo como TensorFlow estén listos.

Nota: Si notaron del paso anterior, pedimos tanto permisos de la galería como permisos de la cámara, así que podríamos importar una fotografía ya tomada o podríamos tomar una a través de la cámara.

<TouchableOpacity
  style={styles.imageWrapper}
  onPress={isModelReady ? selectImageAsync : undefined}
>
  {imageToAnalyze && (
    <View style={{ position: "relative" }}>
      {isModelReady &&
        predictions &&
        Array.isArray(predictions) &&
        predictions.length > 0 &&
        predictions.map((p, index) => {
          return (
            <View
              key={index}
              style={{
                zIndex: 1,
                elevation: 1,
                left: p.bbox[0] * scalingFactor,
                top: p.bbox[1] * scalingFactor,
                width: p.bbox[2] * scalingFactor,
                height: p.bbox[3] * scalingFactor,
                borderWidth: 2,
                borderColor: borderColors[index % 5],
                backgroundColor: "transparent",
                position: "absolute",
              }}
            />
          );
        })}

      <View
        style={{
          zIndex: 0,
          elevation: 0,
        }}
      >
        <Image
          source={imageToAnalyze}
          style={styles.imageContainer}
        />
      </View>
    </View>
  )}

  {!isModelReady && !imageToAnalyze && (
    <Text style={styles.transparentText}>Loading model ...</Text>
  )}

  {isModelReady && !imageToAnalyze && (
    <Text style={styles.transparentText}>Tap here to slect or take a picture</Text>
  )}
</TouchableOpacity>

Hay algunas referencias que debemos tener, igualmente, por lo que creamos tanto los estados como los métodos que están siendo utilizados:

// ...
const [isTfReady, setIsTfReady] = useState(false);
const [isModelReady, setIsModelReady] = useState(false);
const [predictions, setPredictions] = useState(null);
const [imageToAnalyze, setImageToAnalyze] = useState(null);
const model = useRef(null);
// ...
const imageToTensor = (rawImageData) => {
  const { width, height, data } = jpeg.decode(rawImageData, {
    useTArray: true,
  });

  const buffer = new Uint8Array(width * height * 3);
  let offset = 0; // offset into original data
  for (let i = 0; i < buffer.length; i += 3) {
    buffer[i] = data[offset];
    buffer[i + 1] = data[offset + 1];
    buffer[i + 2] = data[offset + 2];

    offset += 4;
  }

  return tf.tensor3d(buffer, [height, width, 3]);
};

const detectObjectsAsync = async (source) => {
  try {
    const imgB64 = await FileSystem.readAsStringAsync(source.uri, {
      encoding: FileSystem.EncodingType.Base64,
    });
    const imgBuffer = tf.util.encodeString(imgB64, 'base64').buffer;
    const rawImageData = new Uint8Array(imgBuffer)
    const imageTensor = imageToTensor(rawImageData);
    const newPredictions = await model.current.detect(imageTensor);

    // Create tanslations
    const translations = await translate(newPredictions.map(prediction => prediction.class), {
      tld: "cn",
      to: "it",
    });
    newPredictions.forEach((element, index) => {
      element.translation = translations[index];
    });
    setPredictions(newPredictions);
    console.log("Detected objects:");
    console.log("-----------------")
    console.log(newPredictions);
  } catch (error) {
    console.log("Error: ", error);
  }
};

const selectImageAsync = async () => {
  try {
    let response = await ImagePicker.launchCameraAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.All,
      allowsEditing: true,
      aspect: [3, 4],
    });

    if (!response.cancelled) {
      // resize image to avoid out of memory crashes
      const manipResponse = await ImageManipulator.manipulateAsync(
        response.uri,
        [{ resize: { width: 900 } }],
        { compress: 1, format: ImageManipulator.SaveFormat.JPEG }
      );

      const source = { uri: manipResponse.uri };
      setImageToAnalyze(source);
      setPredictions(null);
      await detectObjectsAsync(source);
    }
  } catch (error) {
    console.log(error);
  }
};

#.Mostrando los resultados

Ya terminamos con los siguientes pasos:

  • Inicializamos el proyecto móvil.
  • Conectamos a TensorFlow.
  • Utilizamos el modelo de coco-ssd.
  • Tenemos la galería funcionando (con sus permisos).

Nos queda mostrar los resultados en la aplicación y para ello utilizamos y componente jsx adicional y mostramos los resultados, algo similar a lo siguiente:

<View style={styles.predictionWrapper}>
  {isModelReady && imageToAnalyze && (
    <Text style={styles.text}>
      {predictions ? "" : "Please wait..."}
    </Text>
  )}
  {isModelReady &&
    predictions &&
    predictions.map((p, index) => {
      return (
        <View key={index}>
          <Text style={[styles.text, styles.translation]}>
            {`${p.class} (${(p.score * 100).toFixed(2)}%)`}
          </Text>
          <Text style={[styles.text, styles.italian]}>
            {`— 🇮🇹 ${p.translation}`}
          </Text>
        </View>
      );
    })}
</View>

Si quieres, puedes ver el código completo en este Gist de GitHub.


Profile picture

Escrito por Demóstenes García G. (@demogar) Ingeniero de Software y desarrollador web y móvil, basado en Ciudad de Panamá.