Каждое приложение запускается в изолированном окружении: по умолчанию ему недоступны ни данные пользователя, ни большинство аппаратных ресурсов.
Доступ выдаётся явно — списком разрешений в .desktop-файле приложения, секция [X-Application]:
[X-Application]
Permissions=Camera;Pictures
OrganizationName=ru.template
ApplicationName=ImageLab
Разрешения перечисляются через точку с запятой. Основные разрешения:
| Разрешение | Доступ |
|---|---|
Audio |
воспроизведение и запись аудио |
Bluetooth |
Bluetooth-устройства |
Camera |
съёмка фото и видео |
DeviceInfo |
сведения об устройстве |
Documents |
каталог «Документы» (~/Documents) |
Downloads |
каталог «Загрузки» (~/Downloads) |
Internet |
сетевые подключения |
Location |
геопозиция |
Microphone |
запись с микрофона |
Music |
каталог «Музыка» (~/Music) |
NFC |
устройства NFC |
Pictures |
каталог «Изображения» (~/Pictures) |
Videos |
каталог «Видео» (~/Videos) |
UserDirs |
все пользовательские каталоги сразу |
Несколько советов:
Pictures, а не UserDirs;Соответствие каталогов, констант QStandardPaths и разрешений:
| Каталог | QStandardPaths | Разрешение |
|---|---|---|
~/Documents |
DocumentsLocation |
Documents или UserDirs |
~/Downloads |
DownloadLocation |
Downloads или UserDirs |
~/Pictures |
PicturesLocation |
Pictures или UserDirs |
~/Music |
MusicLocation |
Music или UserDirs |
| данные приложения | AppDataLocation |
не требуется |
| кэш приложения | CacheLocation |
не требуется |
Пример забытого разрешения при загрузке картинки: файл существует, путь верный, но
QImage::load()возвращаетfalse, а в галерее картинка видна. ПроверьтеPermissionsв.desktop.
Чтобы пользователь выбрал изображение (документ, файл), используется модуль Sailfish.Pickers — публичный API ОС Аврора:
import QtQuick 2.0
import Sailfish.Silica 1.0
import Sailfish.Pickers 1.0
Page {
id: page
property string selectedPath: ""
Button {
anchors.centerIn: parent
text: qsTr("Выбрать изображение")
onClicked: pageStack.push(imagePickerComponent)
}
Component {
id: imagePickerComponent
ImagePickerPage {
onSelectedContentPropertiesChanged: {
page.selectedPath = selectedContentProperties.filePath
}
}
}
}
selectedContentProperties содержит filePath (полный путь), url, fileName,
mimeType. Помимо ImagePickerPage доступны FilePickerPage (с фильтрами имён),
DocumentPickerPage, MusicPickerPage, ContentPickerPage.
Сам диалог выбора работает вне песочницы приложения, но чтобы открыть выбранный файл из ~/Pictures, приложению нужно разрешение Pictures.
#include <QImage>
QImage image;
if (!image.load(filePath))
return;
// преобразование в 8-битные градации серого
QImage gray = image.convertToFormat(QImage::Format_Grayscale8);
// прямой доступ к пикселям
qint64 sum = 0;
for (int y = 0; y < gray.height(); ++y) {
const uchar *line = gray.constScanLine(y);
for (int x = 0; x < gray.width(); ++x)
sum += line[x];
}
const qreal meanBrightness = qreal(sum) / (qreal(gray.width()) * gray.height());
Полезные операции QImage, которые понадобятся и для нейронок:
scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation) — изменение размера;convertToFormat(QImage::Format_RGB888) — приведение к 24-битному RGB.Элемент Image в QML загружает картинки по URL. Чтобы отдать изображение, сформированное в C++, регистрируется ImageProvider:
class ResultImageProvider : public QQuickImageProvider
{
public:
ResultImageProvider() : QQuickImageProvider(QQuickImageProvider::Image) {}
QImage requestImage(const QString &id, QSize *size,
const QSize &requestedSize) override;
void setImage(const QImage &image);
private:
QMutex m_mutex;
QImage m_image;
};
#include "resultimageprovider.h"
QImage ResultImageProvider::requestImage(const QString &id,
QSize *size,
const QSize &requestedSize)
{
QMutexLocker lock(&m_mutex);
QImage result = m_image;
if (size)
*size = result.size();
if (!requestedSize.isEmpty())
return result.scaled(requestedSize,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
return result;
}
void ResultImageProvider::setImage(const QImage &image)
{
QMutexLocker lock(&m_mutex);
m_image = image;
}
// main.cpp: addImageProvider передаёт владение провайдером движку QML
view->engine()->addImageProvider(QStringLiteral("result"),
new ResultImageProvider);
Image {
cache: false // не кэшировать
source: imageProcessor.resultSource // "image://result/processed/3"
}
Регистрация провайдера:
addImageProvider("result", provider) — регистрирует провайдер под именем "result" в движке QMLimage://result/... будет маршрутизироваться в этот провайдерЗапрос из QML:
Image { source: "image://result/processed/3" } — QML видит схему image:// и понимает, что это не файл, а провайдерresult) и передаёт остаток (processed/3) как параметр idrequestImage() происходит в отдельном потоке загрузки Qt, не в GUI-потоке — отсюда и мьютекс (чтобы предотвратить состояние гонки)Поток данных внутри провайдера:
setImage(result) — кладёт QImage под QMutexLockerrequestImage() — тоже берёт мьютекс, читает m_imagesize заполняется реальными размерамиrequestedSize задан (Image имеет фиксированные width/height), провайдер масштабирует — это эффективнее, чем масштабирование в QMLМеханизм обновления:
Image { cache: false } — отключает внутренний кэш QML по URLНо этого недостаточно, посколько Image не перезагружает картинку, если source не изменился
Решением является менять сам URL: "image://result/processed/3" в "image://result/processed/4"
C++-объект imageProcessor экспортируется в QML через Q_PROPERTY; при смене m_version генерируется сигнал resultSourceChanged() — QML реагирует и перезапрашивает.
Весь интерфейс Qt живёт в одном потоке (главном). Если в этом потоке выполнить
долгую операцию — обработку фотографии, инференс нейросети, чтение большого файла —
интерфейс зависает, не реагирует на касания, не перерисовывается. Допустимый порог блокировки — миллисекунды.
Правило: всё долгое — в отдельный поток; весь UI — только из главного.
// worker.h — исполнитель, живёт в фоновом потоке
class ProcessingWorker : public QObject
{
Q_OBJECT
public slots:
void process(const QString &filePath); // долгая работа здесь
signals:
void finished(const QImage &result, qreal meanBrightness, int elapsedMs);
};
// imageprocessor.h
#include <QObject>
#include <QThread>
#include "processingworker.h"
class ImageProcessor : public QObject
{
Q_OBJECT
Q_PROPERTY(QString resultSource READ resultSource
NOTIFY resultSourceChanged)
public:
explicit ImageProcessor(QObject *parent = nullptr);
~ImageProcessor();
QString resultSource() const { return m_resultSource; }
signals:
void processRequested(const QString &filePath);
void resultSourceChanged();
private slots:
void handleFinished(const QImage &result, qreal meanBrightness, int elapsedMs);
private:
ProcessingWorker *m_worker;
QThread m_thread;
QString m_resultSource;
int m_version = 0;
};
// этот процессор живёт в главном потоке
ImageProcessor::ImageProcessor(QObject *parent) : QObject(parent)
{
// m_thread - приватный объект типа QThread в ImageProcessor
m_worker = new ProcessingWorker;
m_worker->moveToThread(&m_thread);
// удаление worker при остановке потока
connect(&m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
// запрос туда: сигнал контроллера в слот worker (выполнится в фоне)
connect(this, &ImageProcessor::processRequested,
m_worker, &ProcessingWorker::process);
// результат обратно: сигнал worker в слот контроллера (в главном потоке)
connect(m_worker, &ProcessingWorker::finished,
this, &ImageProcessor::handleFinished);
m_thread.start();
}
ImageProcessor::~ImageProcessor()
{
m_thread.quit();
m_thread.wait();
}
Запрещено из фонового потока трогать QML-объекты и виджеты, вызывать методы объектов главного потока напрямую.
| Критерий | Облако | Устройство |
|---|---|---|
| Конфиденциальность | данные покидают устройство | данные остаются локально |
| Работа офлайн | нет | да |
| Задержка | сеть + очередь сервера | миллисекунды–секунды, предсказуемо |
| Стоимость эксплуатации | растёт с числом пользователей | нулевая серверная стоимость |
| Доступные модели | любые | ограничены CPU/памятью устройства |
Для ОС Аврора, которая ориентирована на корпоративный сектор с повышенными требованиями к безопасности данных, локальный инференс часто является естественным выбором.
Поэтому на устройствах используются компактные архитектуры (MobileNet, SqueezeNet, эффективные речевые модели) и квантование — преобразование весов из float32 в int16/int8/int4, уменьшающее размер модели несколько раз и ускоряющее инференс на CPU ценой небольшой (если повезёт) потери точности.
PyTorch / TensorFlow (обучение на сервере или готовая модель)
│ экспорт
▼
ONNX (.onnx — открытый формат обмена моделями)
│ (опционально) квантование
▼
ONNX Runtime (C++ библиотека инференса)
│
▼
Приложение C++/QML на ОС Аврора
ONNX (Open Neural Network Exchange) — открытый формат представления моделей: граф операций + веса. Экспорт из PyTorch реализуется просто:
import torch
import torchvision
model = torchvision.models.mobilenet_v2(weights="IMAGENET1K_V1").eval()
example_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, example_input, "mobilenet_v2.onnx",
input_names=["input"], output_names=["output"],
opset_version=12)
ONNX Runtime — кроссплатформенная библиотека инференса ONNX-моделей, которую ОМП использует в своих официальных примерах. Для ОС Аврора она подключается через пакетный менеджер Conan (про него в следующий раз).
Построить скелет приложения граничных вычислений: выбор изображения из галереи, длительная обработка в фоновом потоке (QThread + worker-объект), отображение результата из C++ через QQuickImageProvider.
.desktop указано разрешение Pictures (и только оно);ImagePickerPage из Sailfish.Pickers;Image.ProcessingWorker (наследник QObject):
process(const QString &filePath);QImage, преобразует в Format_Grayscale8, считает среднюю яркость по пикселям (через constScanLine);QElapsedTimer;finished(QImage, qreal, int);ImageProcessor (живёт в главном потоке):
QThread, перемещает в него worker (moveToThread), корректно останавливает поток в деструкторе (quit() + wait());Q_PROPERTY + NOTIFY): busy, meanBrightness, elapsedMs, resultSource;Q_INVOKABLE void process(const QString &filePath);busy).QQuickImageProvider зарегистрирован в движке QML под именем result;QImage в провайдер и обновляет resultSource с меняющимся суффиксом-версией;Image результата выключен кэш (cache: false).!imageProcessor.busy);BusyIndicator во время обработки;SilicaFlickable).QElapsedTimer timer; timer.start(); ...; int ms = timer.elapsed();QImage мьютексом: requestImage вызывается из потока рендеринга;Image не обновляется после второй обработки, то, возможно, вы забыли помеменять суффикс URL или выключить cache;quit() + wait() в деструкторе процессора.Sailfish.Pickers и др.);