Хобрук: Ваш путь к мастерству в программировании

При сохранении растрового изображения на диск сплошные пути показывают артефакты

[Редактировать: я сделал минимальный проект, чтобы попытаться сузить то, что происходит. Код внизу по-прежнему генерирует те же артефакты при сохранении]

У меня есть приложение, которое рисует простую 2D-геометрию с использованием путей. Все фигуры однотонные, иногда с альфа-каналом ‹ 255, и могут быть украшены линиями. В представлении, которое рисует геометрию, никогда не было проблем с тем, как что-то рисуется. Однако, когда я использую один и тот же код для рисования в растровое изображение, а затем сохраняю его либо в формате JPEG (с качеством 100), либо в формате PNG, всегда возникают одни и те же артефакты в закрашенных областях выходных файлов. Это своего рода пятнистость, которая обычно связана со сжатием JPEG.

Скриншот просмотра: Скриншот активности

Сохраненное изображение: Сохраненный файл изображения

Увеличьте артефакты: Увеличить артефакты

Я пробовал следующее

  • Сохранение в PNG и JPEG
  • Включение и выключение дизеринга и сглаживания
  • Увеличение DPI растрового изображения, а также разрешение растровому изображению использовать API по умолчанию.
  • Применение матрицы, которую я использую в качестве камеры, к геометрическому представлению вместо применения ее к холсту для растрового изображения.
  • Включение и выключение HW Acceleration для всего приложения
  • Использование сторонней библиотеки для сохранения растрового изображения в файл .bmp

Все дают одни и те же артефакты, не делая их ни хуже, ни лучше.

public class MainActivity extends AppCompatActivity {
Context context;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    this.context = getApplicationContext();
}

// button OnClick listener
public void saveImage(View view) {
    new saveBitmapToDisk().execute(false);
}

public Bitmap getBitmap() {
    final int bitmapHeight = 600, bitmapWidth = 600;
    Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
    Canvas bitmapCanvas = new Canvas(bitmap);

    float[] triangle = new float[6];
    triangle[0] = bitmapWidth / 2;
    triangle[1] = 0;
    triangle[2] = 0;
    triangle[3] = bitmapHeight / 2;
    triangle[4] = bitmapWidth / 2;
    triangle[5] = bitmapHeight / 2;

    Path solidPath = new Path();
    Paint solidPaint = new Paint();
    solidPaint.setStyle(Paint.Style.FILL);

    solidPath.moveTo(triangle[0], triangle[1]);

    for(int i = 2; i < triangle.length; i += 2)
        solidPath.lineTo(triangle[i], triangle[i+1]);

    solidPath.close();

    solidPaint.setColor(Color.GREEN);
    bitmapCanvas.drawPath(solidPath, solidPaint);
    return bitmap;
}

private class saveBitmapToDisk extends AsyncTask<Boolean, Integer, Uri> {
    Boolean toShare;

    @Override
    protected Uri doInBackground(Boolean... shareFile) {
        this.toShare = shareFile[0];
        final String appName = context.getResources().getString(R.string.app_name);
        final String IMAGE_SAVE_DIRECTORY = String.format("/%s/", appName);
        final String fullPath = Environment.getExternalStorageDirectory().getAbsolutePath() + IMAGE_SAVE_DIRECTORY;
        File dir, file;

        try {
            dir = new File(fullPath);
            if (!dir.exists())
                dir.mkdirs();

            OutputStream fOut;

            file = new File(fullPath, String.format("%s.png", appName));

            for (int suffix = 0; file.exists(); suffix++)
                file = new File(fullPath, String.format("%s%03d.png", appName, suffix));

            file.createNewFile();
            fOut = new FileOutputStream(file);

            Bitmap saveBitmap = getBitmap();
            saveBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
            fOut.flush();
            fOut.close();
            MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName());

        } catch (OutOfMemoryError e) {
            Log.e("MainActivity", "Out of Memory saving bitmap; bitmap is too large");
            return null;
        } catch (Exception e) {
            Log.e("MainActivity", e.getMessage());
            return null;
        }

        return Uri.fromFile(file);
    }

    @Override
    protected void onPostExecute(Uri uri) {
        super.onPostExecute(uri);
        Toast.makeText(context, "Image saved", Toast.LENGTH_SHORT).show();
    }
}
}

Ответы:


1
  1. Я проверил вашу программу с PNG, и в файле нет артефактов.
  2. Эти артефакты являются результатом сжатия JPEG

Изменить: линия

MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), file.getName(), file.getName());

вызывал преобразование в jpeg.

Правильный способ сохранить изображение

ContentValues values = new ContentValues();
values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis());
values.put(Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
context.getContentResolver().insert(Images.Media.EXTERNAL_CONTENT_URI, values);

Вот моя упрощенная тестовая программа, которая отправляет сгенерированный файл напрямую

public class Test2Activity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    new saveBitmapToDisk().execute();
  }

  public Bitmap getBitmap() {
    final int bitmapHeight = 600, bitmapWidth = 600;
    Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
    Canvas bitmapCanvas = new Canvas(bitmap);

    Paint solidPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    solidPaint.setStyle(Paint.Style.FILL);
    solidPaint.setColor(Color.RED);
    bitmapCanvas.drawCircle(300, 300, 200, solidPaint);

    return bitmap;
  }

  private class saveBitmapToDisk extends AsyncTask<Void, Void, Uri> {
    Boolean toShare;

    @Override
    protected Uri doInBackground(Void... shareFile) {
      Context context = Test2Activity.this;
      try {
        File file = new File(context.getExternalFilesDir(null), "test.png");
        FileOutputStream fOut = new FileOutputStream(file);

        Bitmap saveBitmap = getBitmap();
        saveBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
        fOut.flush();
        fOut.close();
        return Uri.fromFile(file);
      } catch (OutOfMemoryError e) {
        Log.e("MainActivity", "Out of Memory saving bitmap; bitmap is too large");
        return null;
      } catch (Exception e) {
        Log.e("MainActivity", e.getMessage());
        return null;
      }

    }

    @Override
    protected void onPostExecute(Uri uri) {
      Context context = Test2Activity.this;
      Toast.makeText(context, "Image saved", Toast.LENGTH_SHORT).show();

      final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
      intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      intent.putExtra(Intent.EXTRA_STREAM, uri);
      intent.setType("image/png");
      Test2Activity.this.startActivity(intent);
    }
  }
}
02.06.2016
  • Я только что перепроверил на новом устройстве, и я определенно все еще испытываю артефакты, и это определенно не старый файл. И это не миниатюра; Переход к разделу информации указывает на правильные размеры. Возможно ли, что это что-то в среде сборки или что-то в этом роде? 03.06.2016
  • @Project Я добавил в свой ответ упрощенное действие, которое вы должны протестировать. Он создает круглое изображение на прозрачном фоне и открывает общий доступ для отправки этого файла, запускает его на своем устройстве и отправляет сгенерированное изображение по электронной почте себе. Если изображение имеет прозрачный фон, оно кодируется как PNG, если непрозрачное — как jpeg. 03.06.2016
  • Протестировал ваш пример, у него прозрачный фон и нет артефактов. Любопытно, может быть, это каталог, в который я сохраняю, или что-то в этом роде ... будет ковыряться, как только кофе случится. 03.06.2016
  • Итак, оказалось, что строка MediaStore.Images.Media.insertImage(...) была неправильной. Я изменил его на принятый ответ на этот вопрос, и он, наконец, работает . Похоже, код, который я использовал, как-то связан с камерой. Спасибо за помощь в этом; Если вы хотите обновить свой ответ, я могу пометить его как принятый. 03.06.2016
  • @Проект выполнен. Узнал что-то новое для себя 04.06.2016

  • 2

    Подобные артефакты являются естественным и неизбежным следствием сжатия JPEG.

    Они не должны появляться при сжатии PNG. Если вы получаете такие артефакты при создании файла PNG, я бы поспорил, что вы вообще не создаете поток PNG, а скорее поток JPEG в файле с расширением PNG. Ни один приличный декодер не зависит от расширения файла.

    22.05.2016
  • Это происходит и с saveBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fOut); и saveBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); 24.05.2016
  • Чтобы было понятнее; У меня было это как CompressFormat.PNG в течение долгого времени, и я получал такие изображения. Единственная причина, по которой это CompressFormat.JPEG, заключается в том, что я экспериментировал с изменением кода, чтобы посмотреть, получатся ли другие результаты. 24.05.2016

  • 3

    Я заметил две вещи в вашем коде:

    1) Имя файла, которое вы сохраняете, String.format("%s.jpg", appName) или String.format("%s%03d.png", appName, suffix) не зависит от фактической кодировки.

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

    Может быть, вы перепутали себя с 1) или, возможно, 2) вызывает артефакты сжатия, которые вы видите?

    31.05.2016
  • 1) Является ли это следствием моего неправильного редактирования поста 2) Я должен был быть более ясным; Я пробовал это во всех видах плотностей, включая исходную плотность. Я просто вернулся и убедился, что расширение файла было PNG независимо от того, и прокомментировал настройку DPI. Проблема все еще возникает. 01.06.2016
  • В документации даже упоминается, что сохранение в PNG — это операция без потерь, но, возможно, там есть ошибка. Вам следует попробовать написать отдельную тестовую программу, загружающую и сохраняющую растровое изображение, и посмотреть, возникает ли проблема даже там. Это покажет, действительно ли сохранение в PNG происходит без потерь. 01.06.2016
  • Я заменил с помощью Android API использование этой библиотеки для сохранения в виде BITMAP, и я получаю то же самое артефакты. Таким образом, это, похоже, не имеет отношения к сжатию, а к фактическому рисованию в растровое изображение. Что мне кажется довольно странным. 01.06.2016
  • Просто как обновление: я исключал, что я делаю что-то странное с моими методами рисования. Вместо вызова canvasView.cluster.drawCached(bitmapCanvas); я пробовал просто рисовать круги, квадраты и пути непосредственно на растровом изображении. До сих пор получаю эти странные артефакты. Так что не уверен, что еще осталось проверить. Похоже, мне придется создать тестовый проект, чтобы понять это. 01.06.2016
  • Я отредактировал вопрос с помощью действия, которое я сделал, чтобы проверить это в новом проекте. Артефакты все еще возникают при сохранении этого действия. 01.06.2016
  • Новые материалы

    Создание кнопочного меню с использованием HTML, CSS и JavaScript
    Вы будете создавать кнопочное меню, которое имеет состояние наведения, а также позволяет вам выбирать кнопку при нажатии на нее. Финальный проект можно увидеть в этом Codepen . Шаг 1..

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

    Классы в JavaScript
    class является образцом java Script Object. Конструкция «class» позволяет определять классы на основе прототипов с чистым, красивым синтаксисом. // define class Human class Human {..

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

    Как построить любой стол
    Я разработчик программного обеспечения. Я люблю делать вещи и всегда любил. Для меня программирование всегда было способом создавать вещи, используя только компьютер и мое воображение...

    Обзор: Машинное обучение: классификация
    Только что закончил третий курс курса 4 часть специализации по машинному обучению . Как и второй курс, он был посвящен низкоуровневой работе алгоритмов машинного обучения. Что касается..

    Разработка расширений Qlik Sense с qExt
    Использование современных инструментов веб-разработки для разработки крутых расширений Вы когда-нибудь хотели кнопку для установки переменной в приложении Qlik Sense? Когда-нибудь просили..