import React, { useState } from 'react';
import { View, Image, Text, TouchableOpacity, StyleSheet, FlatList } from 'react-native';
import { useImageEmbeddings, CLIP_VIT_BASE_PATCH32_IMAGE } from 'react-native-executorch';
import { launchImageLibrary } from 'react-native-image-picker';
interface ImageWithEmbedding {
uri: string;
embedding: Float32Array;
}
function ImageSimilaritySearch() {
const [images, setImages] = useState<ImageWithEmbedding[]>([]);
const [selectedImage, setSelectedImage] = useState<ImageWithEmbedding | null>(null);
const [similarImages, setSimilarImages] = useState<Array<{ image: ImageWithEmbedding; similarity: number }>>([]);
const { isReady, isGenerating, error, forward } = useImageEmbeddings({
model: CLIP_VIT_BASE_PATCH32_IMAGE,
});
const addImage = async () => {
const result = await launchImageLibrary({ mediaType: 'photo' });
if (result.assets && result.assets[0].uri) {
const uri = result.assets[0].uri;
try {
const embedding = await forward(uri);
setImages(prev => [...prev, { uri, embedding }]);
} catch (err) {
console.error('Failed to generate embedding:', err);
}
}
};
const findSimilar = (target: ImageWithEmbedding) => {
setSelectedImage(target);
const similarities = images
.filter(img => img.uri !== target.uri)
.map(img => ({
image: img,
similarity: cosineSimilarity(target.embedding, img.embedding),
}))
.sort((a, b) => b.similarity - a.similarity);
setSimilarImages(similarities);
};
const cosineSimilarity = (a: Float32Array, b: Float32Array): number => {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
};
if (error) return <Text>Error: {error.message}</Text>;
if (!isReady) return <Text>Loading model...</Text>;
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.button}
onPress={addImage}
disabled={isGenerating}
>
<Text style={styles.buttonText}>
{isGenerating ? 'Processing...' : 'Add Image'}
</Text>
</TouchableOpacity>
<Text style={styles.title}>Image Library ({images.length})</Text>
<FlatList
data={images}
horizontal
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => findSimilar(item)}>
<Image source={{ uri: item.uri }} style={styles.thumbnail} />
</TouchableOpacity>
)}
/>
{selectedImage && (
<View style={styles.results}>
<Text style={styles.title}>Selected Image:</Text>
<Image source={{ uri: selectedImage.uri }} style={styles.selectedImage} />
<Text style={styles.title}>Similar Images:</Text>
<FlatList
data={similarImages}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<View style={styles.similarItem}>
<Image source={{ uri: item.image.uri }} style={styles.thumbnail} />
<Text style={styles.similarity}>
{(item.similarity * 100).toFixed(1)}% similar
</Text>
</View>
)}
/>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20 },
button: { backgroundColor: '#007AFF', padding: 15, borderRadius: 8, marginBottom: 20 },
buttonText: { color: 'white', fontSize: 16, textAlign: 'center' },
title: { fontSize: 18, fontWeight: 'bold', marginVertical: 10 },
thumbnail: { width: 80, height: 80, borderRadius: 8, marginRight: 10 },
selectedImage: { width: 200, height: 200, borderRadius: 8, alignSelf: 'center' },
results: { marginTop: 20 },
similarItem: { flexDirection: 'row', alignItems: 'center', marginVertical: 10 },
similarity: { fontSize: 16, marginLeft: 10 },
});
export default ImageSimilaritySearch;