A Apple adicionou um novo framework no iOS 27 (silenciosamente adicionado na seção Beta), e à primeira vista pode parecer apenas uma camada adicional em cima do Vision. Ele já possui muitas capacidades, mas a parte interessante começa quando paramos de pensar em uma única imagem e onde ela está na foto.
Inteligência de Mídia trabalha com uma coleção. Ela pode manter a análise facial entre as inicializações, agrupar aparições da mesma pessoa em diferentes fotos e encontrar momentos úteis dentro de um vídeo. Vision nos dá observações. Inteligência de Mídia começa a conectar essas observações.
Somente útil, especialmente para aplicativos pesados em fotos. Mas antes que comecemos:
Esta funcionalidade ainda está em beta e pode mudar antes da versão final. Como sempre, estou avisando vocês.
A Inteligência de Mídia atualmente nos dá dois principais analisadores:
AnalisadorFaceGroupAnalisadorVideo
Os nomes são bastante diretos.
AnalisadorFaceGroup detecta faces, armazena os resultados e tenta agrupar aparições que pertencem à mesma pessoa.
AnalisadorVideo trabalha com solicitações como SolicitaçãoKeyFrameAnalysis e SolicitaçãoHighlightAnalysis para encontrar momentos representativos ou interessantes em um vídeo.
O Vision não vai a lugar nenhum. Ele ainda lida com OCR, marcos, poses, máscaras e análise quadro por quadro. Inteligência de Mídia simplesmente fica um nível acima e possui alguns fluxos de trabalho que de outra forma exigiriam persistência, correspondência e muito código colante.
A análise acontece no dispositivo. Os dados faciais não precisam ser enviados para um servidor, e o analisador pode reutilizar seu índice local entre as inicializações. Legal para privacidade, mas não é uma isenção automática. Um grupo gerado ainda não é uma identidade verificada.
Então explique por que o aplicativo analisa pessoas, mantenha os nomes atribuídos pelo usuário fora do diretório de trabalho do analisador e dê aos usuários uma maneira de remover dados gerados.
O exemplo alvo é para iOS 27 e expõe apenas ViewInteligênciaDeMídia, então você pode apresentá-lo a partir de um aplicativo existente sem criar uma cena separada.
E aqui está o primeiro surpreendente prático: um dispositivo físico é necessário. Execute o analisador no Simulador e você pode obter um erro muito descritivo “Não consigo criar contexto”. Obrigado, SDK beta.
O simulador ainda pode renderizar a interface SwiftUI ao redor, mas não reproduz a linha de montagem da Inteligência de Mídia apoiada pelo hardware. O exemplo mostra então ViewConteúdoIndisponível quando compilado para Simulador:
#if targetEnvironment(simulator)
ContentUnavailableView(
"Dispositivo Físico Requerido",
systemImage: "iphone.gen3.slash",
description: Text(
"A análise de Inteligência de Mídia deve ser testada em um dispositivo físico suportado."
)
)
#else
// Conteúdo do iOS 27
#endifTambém não há nenhum booleano genérico público como isNeuralEngineAvailable. Em um dispositivo físico, a inicialização ou o processamento do analisador ainda podem falhar, então o aplicativo deve tratar esses erros como um estado realmente indisponível em vez de mostrar uma roda de carregamento infinita.
Agora podemos finalmente criar o analisador.
AnalisadorFaceGroup parece ser apenas um tipo, mas na verdade lida com quatro diferentes trabalhos:
ingestar ativos de imagem;
detectar faces;
persistir detecções;
agrupar detecções em pessoas.
A própria detecção facial não é a parte mais difícil aqui. Manter os resultados estáveis e agrupá-los posteriormente é onde esta API se torna interessante. A implementação completa será fornecida abaixo. Ela requer alguns structs de ajuda, então nos concentraremos nos pontos principais.
O analisador precisa de um diretório de trabalho gravável. Eu uso Aplicativo de Suporte aqui porque isso é dado persistente gerenciado pelo framework, não um cache temporário:
let diretorioTrabalho = URL.applicationSupportDirectory
.appending(path: "FaceGroupData", directoryHint: .isDirectory)
try FileManager.default.createDirectory(
at: diretorioTrabalho,
withIntermediateDirectories: true
)
let analisador = try FaceGroupAnalyzer(
workingDirectory: diretorioTrabalho
)Reutilize o mesmo diretório entre as inicializações. Caso contrário, você perde a principal vantagem do framework e começa a reconstruir o índice novamente e novamente.
Também, não coloque seus próprios arquivos lá ou tente "corrigir" seu conteúdo manualmente. O diretório pertence ao analisador.
O framework nos dá um ID dedicado
MediaIntelligenceImageAsset.ID, então mantenho-o fortemente tipado em vez de converter tudo para String muito cedo:struct Foto: Identifiable, Hashable {
let id: MediaIntelligenceImageAsset.ID
let nome: String
let url: URL
let imagem: UIImage
}Os ativos são criados com o mesmo ID estável:
let assets = fotosPreparadas.map { foto in
MediaIntelligenceImageAsset(
id: foto.id,
kind: .url(foto.url)
)
}O ID é importante. O método insertOrUpdateAssets(_:) usa-o para entender se a imagem é nova ou se estamos atualizando o mesmo ativo lógico. IDs aleatórios aqui transformariam silenciosamente cada execução em outra importação.
No demo, eu uso arquivos PNG regulares adicionados ao alvo do aplicativo. Nenhuma mágica de catálogo de recursos aqui: apenas photo0.png até photo4.png.
private let nomesRecursos = (0...4).map { "photo\($0)" }Finalmente! Com IA, posso gerar imagens sem direitos autorais e marcas d'água para testes! Não estão felizes em participar nos nossos experimentos de teste?
Cada arquivo é copiado para o diretório local para usar URLs posteriormente:
guard let urlOrigem = Bundle.main.url(
forResource: nome,
withExtension: "png"
) else {
throw DemoError.missingResource("\(nome).png")
}
let urlDestino = diretorioDestino
.appending(path: "\(nome).png")
if !FileManager.default.fileExists(atPath: urlDestino.path) {
try FileManager.default.copyItem(
at: urlOrigem,
to: urlDestino
)
}Por que copiá-los? Porque o FaceGroupAnalyzer funciona bem com ativos baseados em URLs estáveis. Isso também evita a decodificação e recodificação dos mesmos arquivos em cada execução.
O método insertOrUpdateAssets(_:) retorna uma sequência assíncrona. Cada elemento contém um ID de ativo e todos os rostos detectados nesse ativo:
let stream = try await analisador.insertOrUpdateAssets(assets)
var facesPorIDDeAtivo: [
MediaIntelligenceImageAsset.ID: [DetectedFace]
] = [:]
for try await (assetID, faces) in stream {
facesPorIDDeAtivo[assetID] = faces.map { face in
DetectedFace(
id: String(describing: face.id),
assetID: face.assetID,
entityID: face.entityID.map(String.init(describing:)),
bounds: face.bounds
)
}
}Essa distinção é fácil de perder:
Então uma pessoa pode ter muitos IDs de rosto, mas apenas um ID de entidade - pelo menos quando o agrupamento funciona como esperado.
O modelo observável é
@MainActor, pois SwiftUI lê seu estado. Isso faz parte do que era esperado.O que não queremos é acidentalmente executar toda a pipeline de análise no ator UI. O exemplo move esse trabalho para uma função @concurrent:
@MainActor
@Observable
final class PeopleDemoModel {
@concurrent
private func analisarAtivos(
_ ativos: [MediaIntelligenceImageAsset],
diretorioDeTrabalho: URL
) async throws -> AnalysisResult {
// Detecção e agrupamento
}
}O método analisar() prepara as entradas e atualiza o estado da IU. O método analisarAtivos cria o analisador, consome a stream de detecção, executa o agrupamento e retorna um valor que é atribuído novamente no ator principal.
Isto mantém o trabalho pesado longe do ator da IU. Ótimo.
No entanto, há uma armadilha importante de nomenclatura: @concurrent não é uma tarefa em segundo plano do iOS. Ele altera o comportamento do executor, não do ciclo de vida da aplicação. Uma tarefa regular ainda não está garantida a continuar após a suspensão do app.
A detecção e o agrupamento são dois passos separados. Após inserir ou alterar ativos, o analisador geralmente fica obsoleto:
if await analisador.estado == .obsoleto {
try await analisador.atualizar()
}Os estados importantes são:
.pronto— as atribuições de grupo estão atualizadas;.obsoleto— os ativos armazenados foram alterados e o agrupamento deve ser executado novamente;.atualizando— o agrupamento está sendo executado.
Depois da atualização, os grupos são lidos de todasAsFacesPorEntityID e ordenados pelo número de aparições.
A pergunta natural seguinte é a confiança. Quão certo o quadro está de que esses dois rostos pertencem à mesma pessoa?
No momento, a API beta pública não expõe uma pontuação numérica para cada atribuição. Nós apenas recebemos face.entityID.
Isto significa que a IU deve evitar parecer saber mais do que o quadro nos diz. “Pessoas” está bem. “Identidades verificadas” não está. E para um produto real, ferramentas de fusão, divisão e nomeação não são polimento opcional — elas fazem parte da funcionalidade.
Agora o fluxo de reinicialização mantém o diretório de trabalho do analisador e remove apenas seus ativos indexados. Primeiro, ele valida que o caminho é um diretório. Em seguida, cria FaceGroupAnalyzer e chama deleteAllAssets():
let diretorioDeTrabalho = URL.applicationSupportDirectory
.appending(path: "FaceGroupData", directoryHint: .isDirectory)
let ehDiretorio = try? diretorioDeTrabalho.resourceValues(forKeys: [.isDirectoryKey]).isDirectory
if ehDiretorio == nil {
try FileManager.default.createDirectory(
at: diretorioDeTrabalho,
withIntermediateDirectories: true
)
} else if ehDiretorio != true {
throw DemoError.invalidWorkingDirectory
}
let analisador = try FaceGroupAnalyzer(
workingDirectory: diretorioDeTrabalho
)
try await analisador.deleteAllAssets()Existem dois níveis de reinicialização aqui.
deleteAllAssets() remove os ativos indexados, mas mantém a loja do analisador viva. FaceGroupAnalyzer.purge(workingDirectory:) é o martelo maior e remove completamente os dados gerenciados pelo analisador para o diretório.
No botão de reinicialização dentro da demonstração, deleteAllAssets() é suficiente. Não há necessidade de queimar a casa inteira.
O VideoAnalyzer não constrói um banco de dados persistente de rostos. Ele executa objetos de solicitação contra um MediaIntelligenceVideoAsset:
let asset = MediaIntelligenceVideoAsset(url: videoURL)
let keyFrameRequest = KeyFrameAnalysisRequest()
let highlightRequest = HighlightAnalysisRequest()
let (keyFrameResult, highlightResult) = try await VideoAnalyzer.shared.analyze(
asset,
for: keyFrameRequest,
highlightRequest
)A solicitação KeyFrameAnalysisRequest escolhe um momento representativo. A solicitação HighlightAnalysisRequest retorna intervalos interessantes ao longo do cronograma.
O aplicativo ainda possui a reprodução, geração de miniaturas e apresentação. A Inteligência em Mídia nos dá as decisões, não toda a linha de montagem da IU.
O Vision pode processar quadros individualmente, mas então o aplicativo assume a amostragem e agregação temporal. O VideoAnalyzer fornece algumas decisões de nível de cronologia diretamente. E não, essa parte não agrupa rostos do vídeo 🙂
Agora vamos juntar tudo.
O demo tem uma única entrada pública: o MediaIntelligenceView. A sequência é:
exigir iOS 27 e um dispositivo físico;
carregar as imagens
photo0.pngatéphoto4.pngdo pacote de aplicativos;cópias delas para o Application Support;
criar valores estáveis de
MediaIntelligenceImageAsset.ID;executar a detecção e agrupamento fora do
MainActor;publicar rostos e grupos de volta para o SwiftUI;
exibir cada rosto detectado;
abrir cada grupo de pessoas para ver as fotos correspondentes.
Isso é como vai parecer inicialmente:
O modelo usa estados explícitos e progresso opcional:
enum LoadingState: Equatable { case idle case preparing case detecting(current: Int, total: Int) case grouping case finished case failed(String) var progress: Double? { switch self { case let .detecting(current, total) where total > 0: Double(current) / Double(total) case .finished: 1 default: nil } } }A função atual @concurrent retorna um único resultado final, então a UI não recebe progresso real por ativo enquanto a análise está em andamento. O estado está pronto para isso, mas o pipeline ainda não existe.
Uma próxima iteração pode emitir atualizações através de AsyncStream, um ator ou registros de trabalho persistidos. Para cinco imagens demo é bom. Para cinco mil fotos, definitivamente não.
No preparo vai parecer assim (funciona bem rápido eu diria):
O exemplo converte os limites normalizados da face no canto inferior esquerdo para o retângulo de imagem exibido:
CGRect( x: imageRect.minX + normalizedBounds.minX * imageRect.width, y: imageRect.minY + (1 - normalizedBounds.maxY) * imageRect.height, width: normalizedBounds.width * imageRect.width, height: normalizedBounds.height * imageRect.height )Sistemas de coordenadas sempre são uma pequena aventura, então verifique isso contra a SDK beta atual e suas próprias fotos.
E aqui estamos nós! Eu fiz retângulos com bordas verdes para não ser tão estressante:
e a visão detalhada:
O código completo está disponível aqui + imagens estão no comentário.
Use Vision quando precisar de observações diretas: OCR, landmarks, poses, saliência, máscaras ou processamento personalizado de quadros.
Use Media Intelligence quando a funcionalidade é sobre relacionamentos em uma coleção ou decisões ao longo de um cronograma.
Não é uma história de substituição. Os frameworks se complementam.
A detecção facial é apenas a parte visível do álbum People. Persistência, identidade estável, atualizações incrementais, agrupamento, recuperação de interrupções e consultas posteriores são as partes menos glamurosas — e geralmente as mais caras.
O pacote Media Intelligence agrupa esses fluxos de trabalho sem transformar o Vision em uma API de banco de dados com estado. Esta separação faz sentido.
Esta API é adequada para aplicativos que compreendem uma coleção de mídia, e não apenas uma imagem isolada:
Não adicionaria agrupamento facial apenas porque a Apple nos deu um novo framework. O produto ainda precisa ter uma razão clara para analisar pessoas e uma maneira clara de remover os dados gerados. Caso contrário, é apenas uma demonstração impressionante à procura de um problema.
Um grupo não é uma identidade verificada.
A confiança numérica do agrupamento não está publicamente exposta.
Uma pessoa pode ser dividida em vários grupos, ou rostos semelhantes podem ser combinados.
O aplicativo precisa de sua própria camada de nomenclatura e correção.
A testagem realista requer um dispositivo físico suportado.
O simulador não é uma prova válida de desempenho ou capacidade.
Nomes e comportamento da API beta podem mudar.
Manter IDs estáveis de ativos.
Inserir apenas novos ou ativos alterados.
Reutilizar o diretório de trabalho do analisador.
Evitar a recriação completa do índice em cada lançamento.
Mantenha a análise fora
MainActor.Persistir o progresso e fazer as lotes idempotentes.
Salvar resultados parciais antes da expiração.
Testar cancelamento, suspensão, terminação, relançamento e armazenamento baixo.
Codificação feliz!




