Русский
Русский
English
Статистика
Реклама

Image processing

Как с помощью HUAWEI ML Kit интегрировать в приложения стикеры с изображением лица

21.08.2020 06:20:10 | Автор: admin

Общая информация


Сейчас мы повсюду видим милые и смешные стикеры с изображением лица. Они используются не только в приложениях для камер, но и в социальных сетях и развлекательных приложениях. В этой статье я покажу вам, как создать 2D-стикеры с помощью инструмента HUAWEI ML Kit. Скоро мы также расскажем о процессе разработки 3D-стикеров, так что следите за обновлениями!

Сценарии


Приложения для съемки и редактирования фотографий, такие как селфи-камеры и социальные сети (TikTok, Weibo, WeChat и др.), часто предлагают набор стикеров для настройки изображений. С помощью этих стикеров пользователи могут создавать привлекательный и яркий контент и делиться им.

Подготовка


Добавьте репозиторий Maven Huawei в файл на уровне проекта build.gradle


Откройте файл build.gradle в корневом каталоге вашего проекта Android Studio.
image
Добавьте адрес репозитория Maven.
buildscript {    {              maven {url 'http://developer.huawei.com/repo/'}  }    }allprojects {  repositories {             maven { url 'http://developer.huawei.com/repo/'}  }}

Добавьте зависимости SDK в файл на уровне приложения build.gradle


image

// Face detection SDK.implementation 'com.huawei.hms:ml-computer-vision-face:2.0.1.300'// Face detection model.implementation 'com.huawei.hms:ml-computer-vision-face-shape-point-model:2.0.1.300'

Запросите права доступа к камере, сети и памяти в файле AndroidManifest.xml


<!--Camera permission--> <uses-feature android:name="android.hardware.camera" /><uses-permission android:name="android.permission.CAMERA" /><!--Write permission--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><!--Read permission--> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Разработка кода


Настройте анализатор лица


MLFaceAnalyzerSetting detectorOptions;detectorOptions = new MLFaceAnalyzerSetting.Factory()      .setFeatureType(MLFaceAnalyzerSetting.TYPE_UNSUPPORT_FEATURES)      .setShapeType(MLFaceAnalyzerSetting.TYPE_SHAPES)      .allowTracing(MLFaceAnalyzerSetting.MODE_TRACING_FAST)      .create();detector = MLAnalyzerFactory.getInstance().getFaceAnalyzer(detectorOptions);

Получите точки контура лица и передайте их в FacePointEngine


С помощью обратного вызова камеры получите данные камеры, а затем вызовите анализатор лица, чтобы получить точки контура лица, и передайте эти точки в FacePointEngine. Фильтр стикеров сможет использовать их позже.
@Overridepublic void onPreviewFrame(final byte[] imgData, final Camera camera) {  int width = mPreviewWidth;  int height = mPreviewHeight;  long startTime = System.currentTimeMillis();  // Set the shooting directions of the front and rear cameras to be the same.  if (isFrontCamera()){      mOrientation = 0;  }else {      mOrientation = 2;  }  MLFrame.Property property =          new MLFrame.Property.Creator()                  .setFormatType(ImageFormat.NV21)                  .setWidth(width)                  .setHeight(height)                  .setQuadrant(mOrientation)                  .create();  ByteBuffer data = ByteBuffer.wrap(imgData);  // Call the face analyzer API.  SparseArray<MLFace> faces = detector.analyseFrame(MLFrame.fromByteBuffer(data,property));  // Determine whether face information is obtained.  if(faces.size()>0){      MLFace mLFace = faces.get(0);      EGLFace EGLFace = FacePointEngine.getInstance().getOneFace(0);      EGLFace.pitch = mLFace.getRotationAngleX();      EGLFace.yaw = mLFace.getRotationAngleY();      EGLFace.roll = mLFace.getRotationAngleZ() - 90;      if (isFrontCamera())          EGLFace.roll = -EGLFace.roll;      if (EGLFace.vertexPoints == null) {          EGLFace.vertexPoints = new PointF[131];      }      int index = 0;      // Obtain the coordinates of a user's face contour points and convert them to the floating point numbers in normalized coordinate system of OpenGL.      for (MLFaceShape contour : mLFace.getFaceShapeList()) {          if (contour == null) {              continue;          }          List<MLPosition> points = contour.getPoints();          for (int i = 0; i < points.size(); i++) {              MLPosition point = points.get(i);              float x = ( point.getY() / height) * 2 - 1;              float y = ( point.getX() / width ) * 2 - 1;              if (isFrontCamera())                  x = -x;              PointF Point = new PointF(x,y);              EGLFace.vertexPoints[index] = Point;              index++;          }      }      // Insert a face object.      FacePointEngine.getInstance().putOneFace(0, EGLFace);      // Set the number of faces.      FacePointEngine.getInstance().setFaceSize(faces!= null ? faces.size() : 0);  }else{      FacePointEngine.getInstance().clearAll();  }  long endTime = System.currentTimeMillis();  Log.d("TAG","Face detect time: " + String.valueOf(endTime - startTime));}

На изображении ниже показано, как точки контура лица возвращаются с помощью API ML Kit.
image

Определение данных стикера в формате JSON


public class FaceStickerJson {  public int[] centerIndexList;   // Center coordinate index list. If the list contains multiple indexes, these indexes are used to calculate the central point.  public float offsetX;           // X-axis offset relative to the center coordinate of the sticker, in pixels.  public float offsetY;           // Y-axis offset relative to the center coordinate of the sticker, in pixels.  public float baseScale;         // Base scale factor of the sticker.  public int startIndex;         // Face start index, which is used for computing the face width.  public int endIndex;           // Face end index, which is used for computing the face width.  public int width;               // Sticker width.  public int height;             // Sticker height.  public int frames;             // Number of sticker frames.  public int action;             // Action. 0 indicates default display. This is used for processing the sticker action.  public String stickerName;     // Sticker name, which is used for marking the folder or PNG file path.  public int duration;           // Sticker frame displays interval.  public boolean stickerLooping; // Indicates whether to perform rendering in loops for the sticker.  public int maxCount;           // Maximum number of rendering times....}

Сделайте стикер с изображением кота


Создайте файл JSON для стикера с изображением кота и определите центральную точку между бровями (84) и кончиком носа (85) с помощью индекса лица. Вставьте уши и нос кота, а затем поместите файл JSON и изображение в папку assets.
{  "stickerList": [{      "type": "sticker",      "centerIndexList": [84],      "offsetX": 0.0,      "offsetY": 0.0,      "baseScale": 1.3024,      "startIndex": 11,      "endIndex": 28,      "width": 495,      "height": 120,      "frames": 2,      "action": 0,      "stickerName": "nose",      "duration": 100,      "stickerLooping": 1,      "maxcount": 5  }, {      "type": "sticker",      "centerIndexList": [83],      "offsetX": 0.0,      "offsetY": -1.1834,      "baseScale": 1.3453,      "startIndex": 11,      "endIndex": 28,      "width": 454,      "height": 150,      "frames": 2,      "action": 0,      "stickerName": "ear",      "duration": 100,      "stickerLooping": 1,      "maxcount": 5  }]}

Рендеринг стикера в текстуру


Мы выполняем рендеринг стикера в текстуру с помощью класса GLSurfaceView он проще, чем TextureView. Создайте экземпляр фильтра стикеров в onSurfaceChanged, передайте путь стикера и запустите камеру.
@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {  GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);  mTextures = new int[1];  mTextures[0] = OpenGLUtils.createOESTexture();  mSurfaceTexture = new SurfaceTexture(mTextures[0]);  mSurfaceTexture.setOnFrameAvailableListener(this);  // Pass the samplerExternalOES into the texture.  cameraFilter = new CameraFilter(this.context);  // Set the face sticker path in the assets directory.  String folderPath ="cat";  stickerFilter = new FaceStickerFilter(this.context,folderPath);  // Create a screen filter object.  screenFilter = new BaseFilter(this.context);  facePointsFilter = new FacePointsFilter(this.context);  mEGLCamera.openCamera();}

Инициализируйте фильтр стикеров в onSurfaceChanged


@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {  Log.d(TAG, "onSurfaceChanged. width: " + width + ", height: " + height);  int previewWidth = mEGLCamera.getPreviewWidth();  int previewHeight = mEGLCamera.getPreviewHeight();  if (width > height) {      setAspectRatio(previewWidth, previewHeight);  } else {      setAspectRatio(previewHeight, previewWidth);  }  // Set the image size, create a FrameBuffer, and set the display size.  cameraFilter.onInputSizeChanged(previewWidth, previewHeight);  cameraFilter.initFrameBuffer(previewWidth, previewHeight);  cameraFilter.onDisplaySizeChanged(width, height);  stickerFilter.onInputSizeChanged(previewHeight, previewWidth);  stickerFilter.initFrameBuffer(previewHeight, previewWidth);  stickerFilter.onDisplaySizeChanged(width, height);  screenFilter.onInputSizeChanged(previewWidth, previewHeight);  screenFilter.initFrameBuffer(previewWidth, previewHeight);  screenFilter.onDisplaySizeChanged(width, height);  facePointsFilter.onInputSizeChanged(previewHeight, previewWidth);  facePointsFilter.onDisplaySizeChanged(width, height);  mEGLCamera.startPreview(mSurfaceTexture);}

Нарисуйте на экране стикер с помощью onDrawFrame


@Overridepublic void onDrawFrame(GL10 gl) {  int textureId;  // Clear the screen and depth buffer.  GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);  // Update a texture image.  mSurfaceTexture.updateTexImage();  // Obtain the SurfaceTexture transform matrix.   mSurfaceTexture.getTransformMatrix(mMatrix);  // Set the camera display transform matrix.  cameraFilter.setTextureTransformMatrix(mMatrix);  // Draw the camera texture.  textureId = cameraFilter.drawFrameBuffer(mTextures[0],mVertexBuffer,mTextureBuffer);  // Draw the sticker texture.  textureId = stickerFilter.drawFrameBuffer(textureId,mVertexBuffer,mTextureBuffer);  // Draw on the screen.  screenFilter.drawFrame(textureId , mDisplayVertexBuffer, mDisplayTextureBuffer);  if(drawFacePoints){      facePointsFilter.drawFrame(textureId, mDisplayVertexBuffer, mDisplayTextureBuffer);  }}

Получилось! Ваш стикер с изображением лица готов.
Давайте проверим его в действии!
image
Для получения подробной информации перейдите на наш официальный сайт.
Вы также можете посмотреть пример кода.
Подробнее..

KotlinDL 0.2 Functional API, зоопарк моделей c ResNet и MobileNet, DSL для обработки изображений

25.05.2021 12:05:50 | Автор: admin

Представляем вам версию 0.2 библиотеки глубокого обучения KotlinDL.

KotlinDL 0.2 теперь доступен на Maven Central (до этого он лежал на bintray, но закатилось солнышко земли опенсорсной). Появилось столько всего нового: новые слои, специальный DSL для препроцессинга изображений, новые типы датасетов, зоопарк моделей с несколькими моделями из семейства ResNet, MobileNet и старой доброй моделью VGG (рабочая лошадка, впрочем).

В этой статье мы коснемся самых главных изменений релиза 0.2. Полный список изменений доступен по ссылке.

Functional API

Прошлая версия библиотеки позволяла описывать нейронные сети лишь при помощи Sequential API. Например, используя метод Sequential.of(..), вы могли легко описать модель как последовательность слоев и построить VGG-подобную модель.

Однако с 2014 года (эпохи взлета и расцвета подобных архитектур) много воды утекло, и было создано множество новых нейросетей. В частности, стандартным подходом стало использование так называемых остаточных нейросетей (Residual Neural Networks или ResNet), которые решают проблемы исчезающих градиентов (vanishing gradients) и, напротив, взрывающихся градиентов (exploding gradients) а значит, и проблемы деградации обучения нейросети. Подобные архитектуры невозможно описать в виде Sequential API их корректнее представлять в виде направленного ациклического графа (Directed Acyclic Graph). Для задания таких графов мы добавили в версии 0.2 новый Functional API, который позволяет нам описывать модели, подобные ResNet или MobileNet.

Ну что же, давайте построим некое подобие ResNet. Нейросеть будет обучаться на датасете FashionMnist (небольшие изображения модных вещей). Черно-белые изображения размером 28х28 отлично подойдут на старте работы с нейросетями.

val (train, test) = fashionMnist()val inputs = Input(28, 28, 1)val conv1 = Conv2D(32)(inputs)val conv2 = Conv2D(64)(conv1)val maxPool = MaxPool2D(poolSize = intArrayOf(1, 3, 3, 1),strides = intArrayOf(1, 3, 3, 1))(conv2)val conv3 = Conv2D(64)(maxPool)val conv4 = Conv2D(64)(conv3)val add1 = Add()(conv4, maxPool)val conv5 = Conv2D(64)(add1)val conv6 = Conv2D(64)(conv5)val add2 = Add()(conv6, add1)val conv7 = Conv2D(64)(add2)val globalAvgPool2D = GlobalAvgPool2D()(conv7)val dense1 = Dense(256)(globalAvgPool2D)val outputs = Dense(10, activation = Activations.Linear)(dense1)val model = Functional.fromOutput(outputs)model.use {it.compile(optimizer = Adam(),loss = Losses.SOFT_MAX_CROSS_ENTROPY_WITH_LOGITS,metric = Metrics.ACCURACY)it.summary()it.fit(dataset = train, epochs = 3, batchSize = 1000)val accuracy = it.evaluate(dataset = test, batchSize = 1000).metrics[Metrics.ACCURACY]println("Accuracy after: $accuracy")}

Перед вами вывод метода summary(), описывающий архитектуру только что созданной нами модели.

Некоторые не любят сухие отчеты и предпочитают диаграммы. В нашем случае диаграмма типична для всех представителей славного семейства ResNet.

Если вы знакомы с фреймворком Keras, то без особого труда сможете перенести модели, описанные при помощи Functional API, в Keras, используя KotlinDL.

Коллекция предварительно тренированных моделей ResNet и MobileNet

Начиная с релиза 0.2, в Kotlin DL появляется зоопарк моделей (или Model Zoo). По сути, это коллекция моделей с весами, полученными в ходе обучения на большом датасете изображений (ImageNet).

Зачем нужна такая коллекция моделей? Дело в том, что современные сверхточные нейросети могут иметь сотни слоев и миллионы параметров, обновляемых многократно в течении каждой итерации обучения. Тренировка моделей до приемлемого уровня точности (7080%) на таком большом датасете, как ImageNet, может занимать сотни и тысячи часов вычислительного времени большого кластера из видеокарт.

Зоопарк моделей позволяет вам пользоваться уже готовыми и натренированными моделями (вам не придется тренировать их с нуля каждый раз, когда они вам нужны). Вы можете использовать такую модель непосредственно для предсказаний. Также вы можете применить ее для дотренировки части модели на небольшой порции входных данных это весьма распространненная техника при использовании переноса обучения (Transfer Learning). Это может занять десятки минут на одной видеокарте (или даже центральном процессоре) вместо сотен часов на большом кластере.

Доступны следующие модели:

  • VGG16

  • VGG19

  • ResNet50

  • ResNet101

  • ResNet152

  • ResNet50v2

  • ResNet101v2

  • ResNet152v2

  • MobileNet

  • MobileNetv2

Для каждой модели из этого списка доступны функции загрузки конфигурации модели в JSON-формате и весов в формате .h5. Также для каждой модели можно использовать специальный препроцессинг, применявшийся для ее обучения на датасете ImageNet.

Ниже вы видите пример загрузки одной из таких моделей (ResNet50):

// specify the model type to be loaded, ResNet50, for exampleval loader =ModelZoo(commonModelDirectory = File("cache/pretrainedModels"), modelType = ModelType.ResNet_50)// obtain the model configurationval model = loader.loadModel() as Functional// load class labels (from ImageNet dataset in ResNet50 case)val imageNetClassLabels = loader.loadClassLabels()// load weights if required (for Transfer Learning purposes)val hdfFile = loader.loadWeights()

Ну что же, теперь у вас есть сама модель и веса вы можете использовать их по вашему усмотрению.

Внимание! К изображениям, которые вы подаете на вход модели для предсказаний, необходимо применять специальный препроцессинг, о котором мы говорили ранее. Иначе вы получите неверные результаты. Для вызова препроцессинга используйте функцию preprocessInput.

Если вам не нужны предобученные веса, но вы не хотите описывать многослойные модели а-ля VGG или ResNet с нуля, у вас есть два пути: а) просто загрузить конфигурацию модели либо б) взять за основу полный код конструирования модели, написанный на Kotlin, он доступен для каждой из моделей через вызов функции высшего порядка, лежащей в пакете org.jetbrains.kotlinx.dl.api.core.model.

Ниже мы приводим пример использования функции, строящей облегченную версию ResNet50:

val model = resnet50Light(imageSize = 28,numberOfClasses = 10,numberOfChannels = 1,lastLayerActivation = Activations.Linear)

Если вы хотите узнать больше о переносе обучения и использовании зоопарка моделей, советуем этот туториал: вы увидите, как загружается модель VGG19, затем у нее удаляется последний слой, добавляются новые Dense-слои, после чего их веса инициализируются и дообучаются на небольшом датасете, состоящем из изображений кошек и собак.

DSL для предобработки изображений

Python-разработчикам предлагается огромное количество библиотек визуализации и предобработки изображений, музыки и видео. Разработчикам экосистемы языков программирования JVM повезло меньше.

Большинство библиотек для предобработки изображений, найденные на просторах Github и имеющие разную степень заброшенности, так или иначе используют класс BufferedImage, оборачивая его более понятным и согласованным API. Мы решили упростить жизнь Kotlin-разработчиков, предложив им простой DSL, построенный на лямбда-выражениях и объектах-приемниках.

На данный момент доступны следующие функции преобразования изображений:

  • Load

  • Crop

  • Resize

  • Rotate

  • Rescale

  • Sharpen

  • Save

val preprocessing: Preprocessing = preprocess {   transformImage {       load {           pathToData = imageDirectory           imageShape = ImageShape(224, 224, 3)           colorMode = ColorOrder.BGR       }       rotate {           degrees = 30f       }       crop {           left = 12           right = 12           top = 12           bottom = 12       }       resize {           outputWidth = 400           outputHeight = 400           interpolation = InterpolationType.NEAREST       }   }   transformTensor {       rescale {           scalingCoefficient = 255f       }   }}

Весьма популярной техникой при тренировке глубоких сверточных нейросетей является аугментация данных методика создания дополнительных обучающих данных из имеющихся данных. При помощи перечисленных функций можно организовать простейшую аугментацию: достаточно выполнять повороты изображения некоторый угол и менять его размеры.

Если, экспериментируя с DSL, вы поймете, что некоторых функций вам не хватает, не стесняйтесь написать об этом в наш баг-трекер.

Новые слои

В релизе 0.2 появилось много новых слоев. В основном, это обусловлено тем, что они используются в архитектурах ResNet и MobileNet:

  • BatchNorm

  • ActivationLayer

  • DepthwiseConv2D

  • SeparableConv2D

  • Merge (Add, Subtract, Multiply, Average, Concatenate, Maximum, Minimum)

  • GlobalAvgPool2D

  • Cropping2D

  • Reshape

  • ZeroPadding2D*

* Спасибо Anton Kosyakov за имплементацию нетривиального ZeroPadding2D!

Кстати, если вы хотите добавить новый слой, вы можете самостоятельно реализовать его и создать пул-реквест. Список слоев, которые мы хотели бы включить в релиз 0.3, представлен набором тикетов в баг-трекере с пометкой good first issue и может быть использован вами как точка входа в проект.

Dataset API и парочка наследников: OnHeapDataset & OnFlyDataset

Типичным способом прогона данных через нейросеть в режиме прямого распространения (forward mode) является последовательная загрузка батчей в оперативную память, контролируемую языком, а затем в область нативной памяти, контролируемую вычислительным графом модели TensorFlow.

Мы также поддерживаем подобный подход в OnFlyDataset. Он последовательно, батч за батчем, загружает датасет в течений одной тренировочной эпохи, применяя препроцессинг данных (если вы его заранее определили) и аугментацию (если вы ее добавили).

Этот метод хорош, когда оперативной памяти мало, а данных много. Но что, если оперативной памяти более чем достаточно? Это не такой уж редкий случай для задач переноса обучения: датасеты для дообучения могут быть не такими большими, как при тренировке моделей. Также можно получить некоторый прирост в скорости за счет того, что препроцессинг будет применен лишь один раз на этапе формирования датасета, а не при каждой загрузке батча. Если у вас достаточно оперативной памяти, используйте OnHeapDataset. Он будет держать все данные в оперативной памяти не нужно будет повторно считывать их с диска на каждой эпохе.

Набор встроенных датасетов

Если вы только начинаете путешествие в удивительный мир глубокого обучения, мы настоятельно рекомендуем вам строить и запускать ваши первые нейросети на широко известных датасетах, таких как MNIST (набор рукописных цифр), FashionMNIST(набор изображений модных вещей от компании Zalando), Cifar10 (подмножество ImageNet, насчитывающее 50 000 изображений) или коллекцию изображений кошек и собак со знаменитого соревнования Kaggle (по 25 000 изображений каждого класса различных размеров).

Все эти датасеты, как и модели из зоопарка моделей, вы можете загрузить в папку на вашем диске при помощи функций высшего порядка, таких как mnist() и fashionMnist(). Если датасет уже был загружен, заново по сети он грузиться не будет, а будет взят с диска.

Как добавить KotlinDL в проект

Чтобы начать использовать KotlinDL в вашем проекте, просто добавьте дополнительную зависимость в файл build.gradle:

repositories {    mavenCentral()}dependencies {    implementation 'org.jetbrains.kotlinx:kotlin-deeplearning-api:0.2.0'}

KotlinDL можно использовать в Java-проектах, даже если у вас нет ни капли Kotlin-кода. Здесь вы найдете пример построения и тренировки сверточной сети, полностью написанный на Java.

Если вы думаете, что в вашем проекте будет полезен Java API, напишите нам об этом или создайте PR.

Полезные ссылки

Мы надеемся, что вам понравилась наша статья и новые возможности KotlinDL.

Хотите узнать больше о проекте? Предлагаем ознакомиться с Readme или со страничкой проекта на GitHub. А этот туториал поможет вам создать вашу первую нейросеть на Kotlin.

Если вам интересно, как устроен KotlinDL, как он появился и в каком направлении развивается, почему он так похож на Keras, и планируется ли поддержка PyTorch, посмотрите свежее видео от Алексея Зиновьева.

Также мы ждем вас в Slack-канале #kotlindl (инвайт можно получить тут). В нем вы можете задавать вопросы, участвовать в дискуссиях и первыми получать информацию о превью-релизах и новых моделях в зоопарке моделей.

Ваша обратная связь, ваши описания багов и краш-репорты, идеи и комментарии все это очень важно для нас. Мы ждем новых пользователей и контрибьюторов, как начинающих, так и опытных исследователей всех, кому интересны Deep Learning и Data Science на Kotlin, Java и Scala!

Подробнее..

Распознаем номера автомобилей. Разработка multihead-модели в Catalyst

11.06.2021 08:06:47 | Автор: admin

Фиксация различных нарушений, контроль доступа, розыск и отслеживание автомобилей лишь часть задач, для которых требуется по фотографии определить номер автомобиля (государственный регистрационный знак или ГРЗ).

В этой статье мы рассмотрим создание модели для распознавания с помощью Catalyst одного из самых популярных высокоуровневых фреймворков для Pytorch. Он позволяет избавиться от большого количества повторяющегося из проекта в проект кода цикла обучения, расчёта метрик, создания чек-поинтов моделей и другого и сосредоточиться непосредственно на эксперименте.

Сделать модель для распознавания можно с помощью разных подходов, например, путем поиска и определения отдельных символов, или в виде задачи image-to-text. Мы рассмотрим модель с несколькими выходами (multihead-модель). В качестве датасета возьмём датасет с российскими номерами от проекта Nomeroff Net. Примеры изображений из датасета представлены на рис. 1.

Рис. 1. Примеры изображений из датасета

Общий подход к решению задачи

Необходимо разработать модель, которая на входе будет принимать изображение ГРЗ, а на выходе отдавать строку распознанных символов. Модель будет состоять из экстрактора фичей и нескольких классификационных голов. В датасете представлены ГРЗ из 8 и 9 символов, поэтому голов будет девять. Каждая голова будет предсказывать один символ из алфавита 1234567890ABEKMHOPCTYX, плюс специальный символ - (дефис) для обозначения отсутствия девятого символа в восьмизначных ГРЗ. Архитектура схематично представлена на рис. 2.

Рис. 2. Архитектура модели

В качестве loss-функции возьмём стандартную кросс-энтропию. Будем применять её к каждой голове в отдельности, а затем просуммируем полученные значения для получения общего лосса модели. Оптимизатор Adam. Используем также OneCycleLRWithWarmup как планировщик leraning rate. Размер батча 128. Длительность обучения установим в 10 эпох.

В качестве предобработки входных изображений будем выполнять нормализацию и преобразование к единому размеру.

Кодирование

Далее рассмотрим основные моменты кода. Класс датасета (листинг 1) в общем обычный для CV-задач на Pytorch. Обратить внимание стоит лишь на то, как мы возвращаем список кодов символов в качестве таргета. В параметре label_encoder передаётся служебный класс, который умеет преобразовывать символы алфавита в их коды и обратно.

class NpOcrDataset(Dataset):   def __init__(self, data_path, transform, label_encoder):       super().__init__()       self.data_path = data_path       self.image_fnames = glob.glob(os.path.join(data_path, "img", "*.png"))       self.transform = transform       self.label_encoder = label_encoder    def __len__(self):       return len(self.image_fnames)    def __getitem__(self, idx):       img_fname = self.image_fnames[idx]       img = cv2.imread(img_fname)       if self.transform:           transformed = self.transform(image=img)           img = transformed["image"]       img = img.transpose(2, 0, 1)             label_fname = os.path.join(self.data_path, "ann",                                  os.path.basename(img_fname).replace(".png", ".json"))       with open(label_fname, "rt") as label_file:           label_struct = json.load(label_file)           label = label_struct["description"]       label = self.label_encoder.encode(label)        return img, [c for c in label]

Листинг 1. Класс датасета

В классе модели (листинг 2) мы используем библиотеку PyTorch Image Models для создания экстрактора фичей. Каждую из классификационных голов модели мы добавляем в ModuleList, чтобы их параметры были доступны оптимизатору. Логиты с выхода каждой из голов возвращаются списком.

class MultiheadClassifier(nn.Module):   def __init__(self, backbone_name, backbone_pretrained, input_size, num_heads, num_classes):       super().__init__()        self.backbone = timm.create_model(backbone_name, backbone_pretrained, num_classes=0)       backbone_out_features_num = self.backbone(torch.randn(1, 3, input_size[1], input_size[0])).size(1)        self.heads = nn.ModuleList([           nn.Linear(backbone_out_features_num, num_classes) for _ in range(num_heads)       ])     def forward(self, x):       features = self.backbone(x)       logits = [head(features) for head in self.heads]       return logits

Листинг 2. Класс модели

Центральным звеном, связывающим все компоненты и обеспечивающим обучение модели, является Runner. Он представляет абстракцию над циклом обучения-валидации модели и отдельными его компонентами. В случае обучения multihead-модели нас будет интересовать реализация метода handle_batch и набор колбэков.

Метод handle_batch, как следует из названия, отвечает за обработку батча данных. Мы в нём будем только вызывать модель с данными батча, а обработку полученных результатов расчёт лосса, метрик и т.д. мы реализуем с помощью колбэков. Код метода представлен в листинге 3.

class MultiheadClassificationRunner(dl.Runner):   def __init__(self, num_heads, *args, **kwargs):       super().__init__(*args, **kwargs)       self.num_heads = num_heads    def handle_batch(self, batch):       x, targets = batch       logits = self.model(x)             batch_dict = { "features": x }       for i in range(self.num_heads):           batch_dict[f"targets{i}"] = targets[i]       for i in range(self.num_heads):           batch_dict[f"logits{i}"] = logits[i]             self.batch = batch_dict

Листинг 3. Реализация runnerа

Колбэки мы будем использовать следующие:

  • CriterionCallback для расчёта лосса. Нам потребуется по отдельному экземпляру для каждой из голов модели.

  • MetricAggregationCallback для агрегации лоссов отдельных голов в единый лосс модели.

  • OptimizerCallback чтобы запускать оптимизатор и обновлять веса модели.

  • SchedulerCallback для запуска LR Schedulerа.

  • AccuracyCallback чтобы иметь представление о точности классификации каждой из голов в ходе обучения модели.

  • CheckpointCallback чтобы сохранять лучшие веса модели.

Код, формирующий список колбэков, представлен в листинге 4.

def get_runner_callbacks(num_heads, num_classes_per_head, class_names, logdir):   cbs = [       *[           dl.CriterionCallback(               metric_key=f"loss{i}",               input_key=f"logits{i}",               target_key=f"targets{i}"           )           for i in range(num_heads)       ],       dl.MetricAggregationCallback(           metric_key="loss",           metrics=[f"loss{i}" for i in range(num_heads)],           mode="mean"       ),       dl.OptimizerCallback(metric_key="loss"),       dl.SchedulerCallback(),       *[           dl.AccuracyCallback(               input_key=f"logits{i}",               target_key=f"targets{i}",               num_classes=num_classes_per_head,               suffix=f"{i}"           )           for i in range(num_heads)       ],       dl.CheckpointCallback(           logdir=os.path.join(logdir, "checkpoints"),           loader_key="valid",           metric_key="loss",           minimize=True,           save_n_best=1       )   ]     return cbs

Листинг 4. Код получения колбэков

Остальные части кода являются тривиальными для Pytorch и Catalyst, поэтому мы не станем приводить их здесь. Полный код к статье доступен на GitHub.

Результаты эксперимента

Рис. 3. График лосс-функции модели в процессе обучения. Оранжевая линия train loss, синяя valid loss

В списке ниже перечислены некоторые ошибки, которые модель допустила на тест-сете:

  • Incorrect prediction: T970XT23- instead of T970XO123

  • Incorrect prediction: X399KT161 instead of X359KT163

  • Incorrect prediction: E166EP133 instead of E166EP123

  • Incorrect prediction: X225YY96- instead of X222BY96-

  • Incorrect prediction: X125KX11- instead of X125KX14-

  • Incorrect prediction: X365PC17- instead of X365PC178

Здесь присутствуют все возможные типы: некорректно распознанные буквы и цифры основной части ГРЗ, некорректно распознанные цифры кода региона, лишняя цифра в коде региона, а также неверно предсказанное отсутствие последней цифры.

Заключение

В статье мы рассмотрели способ реализации multihead-модели для распознавания ГРЗ автомобилей с помощью фреймворка Catalyst. Основными компонентами явились собственно модель, а также раннер и набор колбэков для него. Модель успешно обучилась и показала высокую точность на тестовой выборке.

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Больше наших статей по машинному обучению и обработке изображений:

Подробнее..

Вкусовщина и AI как мы в Prisma Labs делали объективно субъективный автоматический улучшатель фотографий

09.03.2021 10:13:46 | Автор: admin

Привет, Хабр! Меня зовут Андрей, я занимаюсь R&D в Prisma Labs. В своё время наша команда провела весьма интересное исследование на тему автоматического улучшения фотографии, результатом которого стала фича AutoAdjustment в приложении Lensa, позволяющая в один клик сделать цветокоррекцию фото. В этом посте я хочу поделиться полученным в ходе проекта опытом. Расскажу, в чём заключается сложность этой задачи, где вас могут поджидать нежеланные грабли. Также покажу, на что способен разработанный нашей командой искусственный интеллект. Прочитав этот пост, вы вместе с нами пройдёте тернистый путь от красивой идеи до одной из киллер-фичей популярного приложения. Ну что, погнали?

Зачем вообще всё это нужно?

Почти каждый хоть раз делился собственными фотографиями в социальных сетях. Выкладывая свои фото, мы, конечно же, хотим, чтобы они набрали как можно больше лайков. Для этого часто приходится прибегать к различным техникам и инструментам для коррекции изображений. Так, например, перед тем как выложить фотографию в Instagram, мы можем наложить какой-то фильтр и исправить некоторые настройки фотографии: экспозицию, контраст, температуру, резкость и т.д. Как итог, мы тратим много собственного времени, при том что основная проблема кроется даже не в этом. Корректируя фотографию, мы, к сожалению, не всегда можем объективно оценить, насколько нам получилось её улучшить (особенно если мы с вами не профессиональные фотографы). Так, после наложения какого-либо фильтра вам может показаться, что фотография стала выглядеть лучше, а друзья могут не согласиться, сказав, что она потеряла естественность, и лучше бы вы вообще выложили оригинал. Получается, время было потрачено напрасно! Конечно же, всегда есть возможность обратиться к профессиональному фотографу или ретушеру, и тогда, скорее всего, обработка будет на высоте, но это тоже непросто и не бесплатно. Из этих мыслей и предположений родилась идея: как бы нам помочь пользователям сэкономить их время предложить решение, которое в один клик сделает их фотографии "лучше" (при помощи искусственного интеллекта, разумеется).

Теперь чуть больше по существу, что же мы хотели получить?

В нашем приложении Lensa присутствуют стандартные инструменты для редактирования фотографий: настройка экспозиции, контраста, теней, температуры, светлых участков и т.д. Нашей задачей было построить решение, которое позволит всего в один клик автоматически подобрать значения для каждой из этих настроек так, чтобы после их применения фотография стала нравиться аудитории сильнее, чем оригинал (по крайней мере, большинству). Мы не претендуем на новизну идеи автоматического улучшения фотографии, методов, которые делают что-то похожее, к моменту начала проекта уже существовало множество. Главной особенностью нашего подхода стала идея предсказывать значения настроек для инструментов, доступных через UI в нашем приложении.

Такой подход был выбран по нескольким причинам:

  • Наша модель только рекомендует пользователю изменения, которые он сам после вправе "донастроить". Юзер сможет скорректировать только те значения, которые ему кажутся ошибочными.

  • Пользователь видит, что конкретно исправлял наш искусственный интеллект, на какие составляющие он делал акцент больше всего. В связи с этим подход становится более интерпретируемым юзер понимает, какие настройки повлияли на результат.

На рисунке ниже представлена схема нашей модели.

Схема модели автоматического улучшения фотографииСхема модели автоматического улучшения фотографии

Одна из основных "фишек" нашего редактора состоит в том, что после "автоулучшения" фотография должна остаться естественной. По этой причине некоторые из инструментов вроде зернистости или выцветания (grain/fade) мы исключили из нашего подхода, так как они делают фотографию менее реалистичной (на рисунке выше предсказанные значения нулевые).

Несмотря на понятную идею и мотивацию, было ясно, что задача весьма непростая. Вот только верхушка айсберга проблем, с которыми мы столкнулись уже в самом начале:

  • Мы не могли использовать уже готовые сторонние решения, так как нам нужно было научиться предсказывать настройки именно наших инструментов. К тому же, качество почти всех SOTA подходов оставляло желать лучшего, даже включая статьи с новомодными RL методами.

  • Субъективность оценки решения. Очень сложно придумать численные метрики, которые смогут нам дать понять, что наше решение делает фотографии объективно лучше. Можно оценивать качество фото нейронкой, но даже хваленая NIMA показывала удручающие результаты на наших данных.

  • Проблемы с данными нужна разметка в одном стиле. Существует множество комбинаций настроек, которые позволяют делать фотографии красивыми кому-то нравятся яркие и резкие фотографии, для кого-то такой результат кажется неприемлемым, а нам нужно получать настройки, которые устроят большинство пользователей.

  • Ко всему прочему, мы не хотим, чтобы модель предсказывала специфичный стиль, например, настройки для перевода в чёрно-белые тона. Многие профессиональные фотографы любят делать нестандартную авторскую обработку. В связи с этим поставить чёткое тз для разметки не самая тривиальная задача.

Давайте разбираться в составляющих. Для начала, что представляют из себя наши инструменты?

В предыдущем разделе речь шла о том, что мы хотим автоматически подбирать значения для экспозиции, контраста, теней и других наших инструментов. Но что означают эти настройки, как они работают и что конкретно делают с фотографией? Оказывается, каждый из таких инструментов в разных приложениях работает по-своему. Нет универсального понятия для "контрастности" или "экспозиции", в каждом приложении в эти термины вложено лишь чьё-то собственное представление о том, что же значат эти термины.

Что означают эти названия у нас в Lensa?

Ответ основан на понимании того, что ожидает пользователь, когда выставляет значение теней на 50/100, а значение контрастности на -20/100. Как оказалось, опытный юзер предполагает, что тёмные области (тени) станут светлее, и при этом вся фотография станет более серой (из-за уменьшения контрастности). Ровно так, опираясь на то, чего ждут от нас пользователи, и к чему они привыкли, мы выстраивали эти инструменты.

Подбросим в топку чуть больше математики. Когда инженеры пытаются делать какие-то манипуляции с картинкой, в первую очередь вспоминают про различные цветовые пространства. Самое распространённое RGB, но в нём гораздо сложнее реализовать наши инструменты, чем в тех, где за цвет и яркость отвечают раздельные компоненты. Все операции с изображением мы выполняем в пространстве LAB (L канал отвечает за интенсивность).

Рассмотрим пару примеров наших инструментов, которые направлены в основном на L канал изображения: контраст и тени. На рисунке ниже представлены кривые инструментов (adjustments) контраста и теней для L канала, по OX исходное значение, по OY значение после применения инструмента.

Кривые для контраста и тенейКривые для контраста и теней

Каждая кривая (красная, серая, зелёная) соответствует некоторому значению этого инструмента.

adjusted=A(x, \alpha)

здесьx исходное значение L канала для некоторого пикселя,adjusted во что перейдетx после применения инструмента,\alpha значение "ползунка" инструмента (сила применения),A кривая (функция) инструмента (adjustment). В случае изображения кривые применяются попиксельно.

Теперь про кривые: зеленая кривая соответствует максимальному значению инструмента тому, как будет преобразован L канал для максимального значения ползунка (+100). Красная кривая, напротив, соответствует минимальному (-100). Пунктирная линия отражает преобразование при нулевом значении\alpha(тождественное).

Инжиниринг кривых не самая тривиальная задача!

Разберёмся с формой кривых на примере контраста. Когда вы увеличиваете контраст у картинки (зелёная кривая), то хотите, чтобы пиксели, которые были светлыми, становились еще более светлыми, а пиксели, которые были тёмными, получались еще более тёмными, при этом серые особо не менялись. Ровно это и позволяет нам сделать кривая контраста: значения ближе к 50 (серый) меняются слабо, зато области, которые находятся ближе к квантилям 1/4 и 3/4, изменяются сильнее всего. В случае с красной кривой всё наоборот: белые и чёрные участки тянутся к серому, поэтому зелёная и красная кривые проходят по разные стороны от пунктирной. С тенями всё еще проще, когда вы поднимаете тени, то хотите, чтобы высветились тёмные участки, а светлые участки почти не менялись. К каналам a и b также применяются некоторые преобразования, но для простоты опустим эти детали.

А что с промежуточными значениями?

Можно линейно проинтерполировать результат максимума или минимума с оригиналом (в зависимости от знака альфы). Так, например, если мы хотим узнать, чему будет равен результат при значение ползунка в 50/100, то сначала считаем, чему будет равен результат при максимальном значении, а затем смешиваем с оригиналом с весами 0.5 (формула ниже).

\dfrac{1}{2}x+\dfrac{1}{2}A(x, \alpha_{max})

Обратимость кривых и предлагаемый подход

Одним из ключевых свойств всех построенных функций является их обратимость.

Обратимость кривой инструментов (adjusts)Обратимость кривой инструментов (adjusts)

Если не вдаваться в детали, то обратимость функции (кривой)Aозначает, что для неё можно построить такую функциюA^{-1}, что для каждого значенияxи каждого допустимого значения\alphaверно следующее утверждение:

A(A^{-1}(x, \alpha), \alpha)=x

Допустим, мы построили такие функции, что все они являются обратимыми, что же дальше? Для упрощения задачи давайте представим, что нам нужно предсказать значение для одного инструмента, например, контраста. То есть наша модель должна принимать на вход исходную фотографию юзераxс "плохим" контрастом и предсказывать такое значение\alpha, чтобы применённая с силой\alphaфункция контраста давала на выходе "идеально контрастную" фотографиюI.В каком случае вообще представляется возможным предсказать такое значение\alphaчтобы получитьI? Это выполнимо, если существует некоторое\alpha, для которого справедливо следующее утверждение:

x=A^{-1}(I, \alpha)

Это предположение является одним из самых важных моментов в нашем подходе.

Более простым языком, мы предполагаем, что все недостатки фотографии пользователя, которые мы хотим исправить, ограничены обратными функциями наших инструментов. Все дальнейшие рассуждения опираются на это предположение.

Допустим, у нас есть сет таких "идеальных" фотографийI. Тогда, "испортив" их обратной функцией для данного инструмента (в нашем случае контраста), мы получим обучающую выборку с триплетами\big(I,\alpha,x=A^{-1}(x,\alpha)\big)и будем учить нашу модель предсказывать\alphaпо входуx.

Но у нас же несколько инструментов!

Для случая нескольких инструментов предположение легко обобщается: все недостатки, которые мы хотим исправить в фотографии, будут ограничены композицией обратных функций наших инструментов.

\begin{cases}x_{0}=I\\x_{i}=A^{-1}_{N-i+1}(x_{i-1}, \alpha_{N-i+1}),\ \ \ i=1...N\end{cases}

Если у насNинструментов, тоx_{N} результат применения композиции их обратных функций.

Ниже представлена иллюстрация парадигмы с триплетами для случая одного инструмента.

Схема обучения модели с триплетамиСхема обучения модели с триплетами

На рисунке выше CI=InvA(I, alpha) искажение "идеальной" фотографии путём наложения некоторой обратной функции со значением alpha. M(CI) предсказание обучаемой модели.

Мы выбрали подход к обучению с ухудшением идеального фото с помощью обратных преобразований по нескольким причинам:

  • Идеологически каждый инструмент (эджаст) должен исправлять только искажения от собственной обратной функции (композиция эджастов от композиции обратных). То есть инструмент контраста направлен на то, чтобы исправлять недочеты именно контраста на фотографии, но никак не температуры или другого инструмента. Всё ещё не удаётся избавиться того, что некоторые эджасты скоррелированы меняя, например, контраст, вы так или иначе влияете на тени (об этом дальше).

  • При таком подходе из одной идеальной фотографии можно получить много плохих, что позволяет нам сгенерировать больше данных. Данные в этой задаче (как и во многих других) весьма важная составляющая.

  • В таком подходе на стадии обучения нам не нужна supervised разметка пар, достаточно только "идеальных" фотографий.

Важнейшая составляющая. Откуда мы брали данные для обучения?

Есть же размеченные данные, что с ними не так?

В начале работы над проектом мы пытались использовать некоторые open-source данные от фотографов вроде MIT-Adobe FiveK Dataset (его используют в большинстве статей по автоулучшению фото). Достаточно быстро мы поняли, что все найденные open-source датасеты нам не подходят по нескольким причинам:

  • Наш таргетинг селфи и портретные фото, во всех открытых датасетах таких изображений малая доля.

  • В датасетах вроде MIT FiveK данные размечены очень специфично множество фотографий выглядят хоть и эффектно, но весьма неестественно заметно, что они были отредактированы.

Мы пробовали размечать данные командой, но столкнулись с проблемой. Каждый выбирал настройки, исходя из собственных вкусов, поэтому в данных не было видно явного тренда, и получившаяся разметка была очень разнородной и даже противоречивой. В связи с этим мы выбрали одного асессора, и в итоге весь датасет был размечен одним человеком с экспертизой в области фото (будем называть его "судьёй"). После этого нашей задачей было уже не обучить объективный "улучшатель" фотографий, а научить модель размечать фотографии так же, как наш судья.

По итогу в датасет попало порядка 1000 фотографий (оригиналы, идеалы и конфиги). Немного, но этого оказалось достаточно.

Одна за всех или все за ...

В начале проекта мы пытались обучать одну нейронную сеть, которая принимает на вход картинку, испорченную композицией всех обратных преобразований, и предсказывает конфиг целиком (значения для всех инструментов). С таким подходом мы не смогли добиться нужного нам качества по нескольким причинам:

  • Эффекты от наложения некоторых инструментов очень скоррелированы. Существует множество конфигов, применяя которые, мы получаем почти одинаковый выход. Обучаться модели с таким условием тяжелее нужно учить все инструменты одновременно. Более того, нужно выучить не только правильный конфиг, а ещё понять, как применение всего этого конфига (всех инструментов одновременно) влияет на фотографию.

  • Модель почти всегда видит сложные сэмплы если мы сразу портим фотографию композицией всех обратных инструментов, то в большинстве случаев модели на вход будет приходить изображение, на котором плохо абсолютно всё: плохая экспозиция, плохой контраст, плохие тени и т.д. Сэмплировать искажения так же, как "in the wild", мы не можем, так как не знаем, как выглядит их априорное распределение в реальном мире.

Поэтому мы решили обучать по одной модели на каждый инструмент (модели у нас были очень маленькие и быстрые, мы могли себе это позволить). Мы выбрали N инструментов и обучали N нейронных сетей, где каждая сеть была ответственна только за свой инструмент обучалась исправлять только его. У инструментов есть определённый порядок, в котором они применяются в приложении, поэтому все эти обученные нейронные сети также применяются последовательно, и результат выхода одной модели (предсказанное значение инструмента) сначала применяется к картинке, а затем результирующее изображение подаётся на вход следующей модели.

У такого подхода тоже есть свои проблемы:

  • Во время обучения определённой модели, например, контраста, сеть не видит фотографии с испорченными тенями, что не является правдой для in the wild. Мы пробовали добавлять аугментациями искажения других инструментов (кроме того, который учим), но опять столкнулись с проблемой корреляции эффектов и усложнили каждой модели задачу.

  • Данный подход сложнее в интеграции на девайс и гораздо более трудозатратен в плане обучения: вам нужно последовательно обучить N сеток вместо всего одной.

Несмотря на эти минусы, такой пайплайн показал на практике результаты лучше, чем подход с одной моделью для всех инструментов.

Функция потерь. Картинки укажут вам более правильный путь, чем конфиги

Сначала мы пробовали, используя регрессионный лосс на паре (predicted_alpha, gt_alpha), учить модель предсказывать верное значение инструмента, но наступили на очередные грабли. У такого подхода есть как минимум два минуса.

Во-первых, как упоминалось выше, может существовать несколько различных конфигов для инструментов, которые приводят к одному и тому же результату. Но, если мы учим отдельно по одной модели на каждый инструмент при фиксированных остальных инструментах (которые не участвуют в обучении), найдётся ровно один правильный ответ для восстановления искажений инструмента, под который мы обучаем модель. Так что для подхода "одна модель под один инструмент" это не стало серьёзным препятствием.

Вторая проблема оказалась более серьёзной, и здесь нас уже не спас подход с обучением своей сетки под каждый инструмент. Допустим, мы хотим исправить насыщенность картинки. Мы знаем, что оригинальное изображение было испорчено со значением -90, а мы предсказали 50. Правда ли, что вне зависимости от исходного изображения мы всегда должны одинаково штрафовать нашу модель? Оказывается, что это не так.

За наглядным примером проблемы и ходить далеко не нужно!
Искажение насыщенности на -90 для двух разных оригиналовИскажение насыщенности на -90 для двух разных оригиналов

В приведённом примере абсолютная разница между предсказанным и истинным значениями одинакова для обоих случаев, при этом мы явно должны штрафовать модель для Case 2 сильнее (мы не восстановим оригинал с предсказанием 50). В то же время, в случае Case 1 для любого предсказания модели мы получаем одинаковый результат в точности оригинал (изменение насыщенности не меняет чёрно-белую фотографию). Получается, что мы должны штрафовать не за разницу в конфигах, а за то, насколько результат применения предсказанного конфига отличается от желанного изображения.

В зависимости от входной фотографии небольшое изменение в предсказанных значениях для инструментов может как значительно влиять на результат, так и не влиять вовсе.

Получается, что функция потерь должна штрафовать разницу между изображениями, а не между конфигами. Например, мы можем использовать L2-норму между изображениями:

Loss=MSE(I, A(M(CI))

Важно отметить, что предложенная функция потерь MSE между изображениями, дифференцируема по предсказанному значению, так как каждая наша функция каждого инструмента дифференцируема по нему.

Мы обучили модель. Как понять, что она делает что-то адекватное?

Валидация результатов работы обученной модели это, наверное, самая нетривиальная часть всего проекта. Помимо метрик (о которых речь пойдёт чуть ниже) мы использовали разные эвристики для того, чтобы понять, насколько разумно ведет себя модель.

Первая эвристика основана на идее о том, что модель не должна корректировать свои же предсказания. Мы назвали такое свойство "сходимостью".

Cходимость первый признак адекватности модели!

Допустим, мы обучили модель для улучшения контрастности. Подаём некоторую фотографию (лучше ту, на которой есть явные проблемы с контрастом) в нашу модель. Модель предсказывает некоторое число P1. Затем применяем к этой фотографии инструмент контраста со значением P1 и получаем новую исправленную фотографию. Подаём её в модель,получаем число P2 и т.д. Так вот, мы считали, что модель является "адекватной", если эта последовательность (P1, P2, P3, ) достаточно быстро сходится. В идеале значения (P2, P3, ) должны быть около нуля.

Иллюстрация сходимости моделиИллюстрация сходимости модели

Второе наблюдение чем сильнее мы испортили фотографию, тем большее (по абсолютной величине) значение наша модель должна предсказывать.

Хорошая модель монотонная модель

На рисунке выше по оси X отложено значение, с которым мы портили фотографию, а по оси Y предсказание модели. Видно, что модель не идеально предсказывает значения, с которыми портится фотография, но отношение порядка сохраняет. Связано это с тем, что модели достаточно просто понять, что на картинке нужно поднять или опустить контрастность и масштаб этого значения (относительно своего априорного идеала), а вот точное абсолютное значение угадать сложно (ещё один пунктик в сторону того, что обучать с функцией потерь именно на конфиги очень сложно). Также во время обучения мы старались подбирать такие распределения для параметров искажения (на основе размеченных данных), чтобы фотографии всё ещё выглядели более-менее естественными, поэтому модель не видела совсем испорченные фотографии (хвосты кривой).

Пристегнитесь, сдедующая остановка используемые метрики!

Долгое время в качестве метрик мы использовали только визуальный тест. Заключался он в следующем: мы показывали нашему судье две фотографии и просили выбрать, какая выглядит лучше. При этом мы также просили дать оценку от 1 до 5 (насколько выбранный вариант выглядит лучше конкурента). Это позволяло нам как сравнивать обученные модели, так и понимать, насколько мы далеки от разметки судьи. Важнейшей проблемой такой метрики является то, что этот процесс совершенно не автоматизирован и требует слишком много времени и усилий каждый раз нам нужно было строить несколько кандидатов (обучать несколько моделей) и для каждого нового кандидата просить нашего судью проходить такой визуальный тест. По этой причине нам пришлось думать над альтернативными метриками хотя бы для уменьшения количества проводимых визуальных тестов (для отсеивания заведомо неудачных моделей).

Итак, мы хотим придумать метрики, которые помогут нам ответить на вопрос, насколько наши новые модели стали ближе к поведению судьи.

Первая метрика была построена с целью понять, насколько сильно изменится результат обработки, если в настройках судьи (gt config) заменить значение одного инструмента, например, контраста, на предсказанное моделью значение. Схема расчёта метрики приведена на рисунке ниже.

Схема подсчёта первой используемой метрикиСхема подсчёта первой используемой метрики

Спустя некоторое время мы поняли, что у такой метрики есть один важный недостаток. Допустим, наша модель на схеме выше (при расчёте метрики) предсказала\alphaдля контраста. У наших инструментов есть четкий порядок применения, и проблема состоит в том, что, на самом деле, контраст идёт вторым в очереди (то есть до теней), а не последним. Даже с учётом того, что мы сохраняем порядок применения всех инструментов после предсказания, мы "подсовываем" модели не совсем истинный вход. В реальном мире (в самом редакторе), даже если обученные модели для экспозиции и теней будут всегда предсказывать правильные значения, модель для контраста, скорее всего, не предскажет то же самое значение\alpha, так как на вход поступит изображение с ещё не исправленными тенями (исправление теней влияет на контраст фотографии).

По этой причине мы разработали ещё одну метрику, которая более честно эмулирует процесс применения всех инструментов при её подсчёте полностью сохраняется порядок всех инструментов, а также совпадают входные изображения для всех моделей. В итоге предсказанные значения во время подсчёта метрики совпадут с теми, которые будут выдавать наши модели, если эту же фотографию загрузят к нам в приложение. В связи с этим за целевую мы стали использовать именно вторую метрику (далее речь пойдёт о ней).

Предположим, мы хотим настраивать экспозицию, контраст и светлые участки. Мы знаем, что сначала идёт экспозиция, затем контраст и только после настройка для светлых участков. Пусть у нас уже есть какое-то приближение для нашего решения модели, которые предсказывают значения для всех указанных инструментов (в самом начале это могут быть просто константные нулевые предсказания). Допустим, мы выучили новую модель для светлых участков. Чтобы проверить, сможет ли только она улучшить качество всего имеющегося пайплайна, мы заменим в лучшем наборе ровно одну модель по светлым участкам и проверим, стал ли предсказанный результат ближе к разметке судьи. Если да, то мы обновляем в лучшем множестве старую модель на новую и переходим к контрасту уже с новой зафиксированной моделью по светлым участкам (затем только к экспозиции).Схема одного предлагаемого шага обновления лучшего набора моделей представлена ниже.

Схема подсчёта второй используемой метрикиСхема подсчёта второй используемой метрики

Мы можем зациклить этот процесс и после экспозиции вернуться к светлым участкам. Зацикливание может быть полезно в случае, если после того, как мы обновили модель по светлым участкам, мы также обновили модель для предшествующего инструмента (все инструменты, которые идут раньше, влияют на последующие).

В качестве критерия можно использовать любую меру сходства между изображениями, например, MSE (как на рисунке выше). Не стоит интерпретировать абсолютную величину этой метрики. Её значение поможет вам понять, приводит ли замена старой модели на новую к улучшению качества всего пайплайна.

Благодаря разработанным метрикам нам удалось снизить количество проводимых визуальных тестов, мы научились автоматически отсеивать заведомо неперспективные решения, а также выбирать множество лучших моделей из обученных. Всё это значительно ускорило разработку.

Везде грабли-грабли-грабли, а что же мы получили в итоге?

Если долго мучиться, всё обязательно получится! Нам удалось построить модели, которые устроили нас по визуальному качеству, удовлетворяли нашим эвристикам и показывали позитивные результаты на приведённых метриках. Сейчас эти модели встроены в наш инструмент автоулучшения фотографии (Autoadjustments) в приложении Lensa. Ниже парочка примеров селфи с результатами работы пайплайна. Ждём вас в гости в приложении, если подумали, что это "черри-пики" :)

Оказалось, что обученные модели весьма стабильны и работают очень неплохо даже для видео, притом что мы во время обучения никак не требовали от них устойчивости между кадрами (лишь использовали некоторые аугментации). Ещё более интересный факт: модели способны показывать хорошее визуальное качество не только на селфи и портретных фотографиях, но и на изображениях с природой, пейзажами, зданиями и прочими категориям, где нет человека, несмотря на то что в датасете были фотографии только с людьми. Пара примеров представлена ниже.

Объяснить это можно тем, что модели у нас весьма небольшие и скорее не оперируют сущностью типа человека, а понимают, как нужно исправить фотографию из более низкоуровневых фичей вроде общей яркости или насыщенности.

Я рассказал вам про одну из наших интереснейших технологий. На собственном опыте показал, как уберечь себя от лишних шишек (мы уже набили их за вас). Берясь за проекты, связанные с обработкой и улучшением фото, формулируйте задачи как можно точнее и понятнее, чтобы лишний раз не натыкаться на проблему субъективизма.

Почти всегда ваша задача состоит не в том, чтобы построить решение, которое будет нравиться всем (это утопия в подобных проектах), а придумать, как угодить нужной аудитории.

От момента начала проекта до релиза финальной версии решения прошёл почти целый год, но для нас игра несомненно стоила свеч, так как полученный функционал идеально ложится в идеологию нашего продукта с улучшением фотографии в один клик. У нас осталось еще много идей, которые мы обязательно попробуем, когда вернёмся к работе над этим проектом, а после я обязательно вам про них расскажу :)

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru