захват наложения с использованием предварительного просмотра CameraX

#android #android-studio #android-layout #camera #android-camerax

#Android #android-studio #android-макет #камера #android-camerax

Вопрос:

Я пытаюсь сделать снимок с наложением, включенным в захват изображения. Я смог установить наложение на previewView использование cameraView.overlay.add(binding.textView) . Как бы то ни было, он не сохранялся при попытке сохранить изображение, при imageCapture этом сохранялось только изображение, а не наложение. Как мне сохранить изображение с включенным наложением с помощью PreviewView камеры x.

Пожалуйста, не помечайте это как дубликат. Я много исследовал, и большинство примеров в Интернете используют старый camera api, который не применяется к библиотеке camera x. Любая помощь приветствуется. Заранее спасибо.

Вот мой код

 
           <FrameLayout
                android:id="@ id/camera_wrapper"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                app:layout_constraintTop_toTopOf="@id/space1"
                app:layout_constraintBottom_toBottomOf="@id/space">
    
                <androidx.camera.view.PreviewView
                    android:id="@ id/camera_view"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent" />
    
                <TextView
                    android:id="@ id/text_view"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:text="Hello world"
                    android:textSize="42sp"
                    android:textColor="@android:color/holo_green_dark"/>
    
            </FrameLayout>

  
   private lateinit var outputDirectory: File
  private lateinit var cameraExecutor: ExecutorService
  private var preview: Preview? = null
  private var lensFacing: Int = CameraSelector.LENS_FACING_FRONT
  private var imageCapture: ImageCapture? = null
  private var camera: Camera? = null
  private var cameraProvider: ProcessCameraProvider? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    outputDirectory = getOutputDirectory()
    cameraExecutor = Executors.newSingleThreadExecutor()
  }

  private fun setupCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())

    cameraProviderFuture.addListener(
      Runnable {
        // Used to bind the lifecycle of cameras to the lifecycle owner
        cameraProvider = cameraProviderFuture.get()

        // Get screen metrics used to setup camera for full screen resolution
        val metrics = DisplayMetrics().also { binding.cameraView.display.getRealMetrics(it) }
        Timber.d("Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}")

        val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels)
        Timber.d("Preview aspect ratio: $screenAspectRatio")

        val rotation = binding.cameraView.display.rotation

        // CameraProvider
        val cameraProvider = cameraProvider
          ?: throw IllegalStateException("Camera initialization failed.")

        // CameraSelector
        val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()

        // add text overlay *---------*
        binding.cameraView.overlay.add(binding.textView)

        // Preview
        preview = Preview.Builder()
          // We request aspect ratio but no resolution
          .setTargetAspectRatio(screenAspectRatio)
          // Set initial target rotation
          .setTargetRotation(rotation)
          .build()

        // ImageCapture
        imageCapture = ImageCapture.Builder()
          .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
          // We request aspect ratio but no resolution to match preview config, but letting
          // CameraX optimize for whatever specific resolution best fits our use cases
          .setTargetAspectRatio(screenAspectRatio)
          // Set initial target rotation, we will have to call this again if rotation changes
          // during the lifecycle of this use case
          .setTargetRotation(rotation)
          .build()

        // Must unbind the use-cases before rebinding them
        cameraProvider.unbindAll()

        try {
          // A variable number of use-cases can be passed here -
          // camera provides access to CameraControl amp; CameraInfo
          camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
          // Attach the viewfinder's surface provider to preview use case
          preview?.setSurfaceProvider(binding.cameraView.surfaceProvider)
        } catch (exc: Exception) {
          Toast.makeText(requireContext(), "Something went wrong. Please try again.", Toast.LENGTH_SHORT).show()
          findNavController().navigateUp()
        }
      },
      ContextCompat.getMainExecutor(requireContext())
    )
  }

private fun takePhoto() {
    imageCapture?.let { imageCapture ->

      // Create output file to hold the image
      val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)

      // Setup image capture metadata
      val metadata = ImageCapture.Metadata().apply {

        // Mirror image when using the front camera
        isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
      }

      // Create output options object which contains file   metadata
      val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
          .setMetadata(metadata)
          .build()

      // Setup image capture listener which is triggered after photo has been taken
      imageCapture.takePicture(outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
        override fun onError(exc: ImageCaptureException) {
          Timber.e(exc, "Photo capture failed: ${exc.message}")
        }

        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
          val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
          Timber.d("Photo capture succeeded: $savedUri")

          // Implicit broadcasts will be ignored for devices running API level >= 24
          // so if you only target API level 24  you can remove this statement
          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            requireActivity()
                .sendBroadcast(Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri))
          }


          // If the folder selected is an external media directory, this is
          // unnecessary but otherwise other apps will not be able to access our
          // images unless we scan them using [MediaScannerConnection]
          val mimeType = MimeTypeMap.getSingleton()
              .getMimeTypeFromExtension(savedUri.toFile().extension)
          MediaScannerConnection.scanFile(
              context,
              arrayOf(savedUri.toFile().absolutePath),
              arrayOf(mimeType)
          ) { _, uri ->
            Timber.d("Image capture scanned into media store: $uri")
          }
        }
      })
    }

  }


  

Ответ №1:

Вы должны наложить текст на изображение самостоятельно. Я бы предложил использовать takePicture (Executor, …), который помещает Jpeg в память; затем наложите свой текст, используя одну из библиотек (не входящих в состав Android framework, ни в Jetpack), и сохраните результат в файле.

Если вы можете пойти на компромисс с качеством изображения, вы можете нарисовать Jpeg на растровом холсте и нарисовать свой текст сверху.

Ответ №2:

Используйте этот плагин https://github.com/huangyz0918/AndroidWM . Кроме того, если вы хотите создать свой собственный, это может помочь вам обратиться к.

Простое использование, как показано ниже

 WatermarkText watermarkText = new WatermarkText(inputText)
            .setPositionX(0.5)
            .setPositionY(0.5)
            .setTextColor(Color.WHITE)
            .setTextFont(R.font.champagne)
            .setTextShadow(0.1f, 5, 5, Color.BLUE)
            .setTextAlpha(150)
            .setRotation(30)
            .setTextSize(20);

val bmFinal: Bitmap = WatermarkBuilder
                            .create(applicationContext, capturedImageBitmap)
                            .loadWatermarkText(watermarkText)
                            .watermark
                            .outputImage

// ##### Then save it #########

fun saveImage(bitmap: Bitmap, photoFile: File) {
    val output: OutputStream = FileOutputStream(photoFile)
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
    output.flush()
    output.close()
    Toast.makeText(
        this@MainActivity,
        "Imaged Saved at ${photoFile.absolutePath}",
        Toast.LENGTH_LONG
    ).show()
}

val photoFile = File(
            outputDirectory,
            SimpleDateFormat(
                FILENAME_FORMAT, Locale.US
            ).format(System.currentTimeMillis())   ".jpg"
        )

saveImage(bmFinal, photoFile)
  

Ответ №3:

Может быть, вы можете использовать класс CameraSource и поместить свой предварительный просмотр / наложение внутрь :

 val cameraSource = CameraSource.Builder(requireContext(),FakeDetector()).build()
cameraSource.start(your_preview_overlay) 
  

И после того, как у вас есть API :

 takePicture(CameraSource.ShutterCallback shutter, CameraSource.PictureCallback jpeg) 
  

Источник камеры (https://developers.google.com/android/reference/com/google/android/gms/vision/CameraSource ) предназначен для обнаружения, но вы можете создать поддельный детектор (ничего не обнаруживать).

Ответ №4:

ответ @alexcohn является предпочтительным, если вы не можете позволить себе потерять качество. Однако, если качество не имеет большого значения, вы можете это сделать.

 <FrameLayout
            android:id="@ id/camera_wrapper"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toTopOf="@id/space1"
            app:layout_constraintBottom_toBottomOf="@id/space">

            <androidx.camera.view.PreviewView
                android:id="@ id/camera_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <ImageView
                android:id="@ id/selfie"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                android:visibility="gone"
                tools:visibility="visible"
                tools:background="@color/gray" />

            <ImageView
                android:id="@ id/overlay"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
                tools:src="@drawable/full_frame_gd" />

        </FrameLayout>
  

PreviewView имеет встроенную функцию, которая дает вам растровое изображение предварительного просмотра

 val bitmap = binding.cameraView.bitmap
binding.selfie.setImageBitmap(bitmap)
binding.selfie.visibility = View.VISIBLE
cameraExecutor.shutdown()
binding.cameraView.visibility = View.GONE
  

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

Отсюда вы можете просто сделать снимок экрана двух комбинированных изображений, подобных этому

 private fun captureView(view: View, window: Window, bitmapCallback: (Bitmap)->Unit) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      // Above Android O, use PixelCopy
      val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
      val location = IntArray(2)
      view.getLocationInWindow(location)
      PixelCopy.request(
          window,
          Rect(
              location[0],
              location[1],
              location[0]   view.width,
              location[1]   view.height
          ),
          bitmap,
          {
            if (it == PixelCopy.SUCCESS) {
              bitmapCallback.invoke(bitmap)
            }
          },
          Handler(Looper.getMainLooper()) )
    } else {
      val tBitmap = Bitmap.createBitmap(
          view.width, view.height, Bitmap.Config.RGB_565
      )
      val canvas = Canvas(tBitmap)
      view.draw(canvas)
      canvas.setBitmap(null)
      bitmapCallback.invoke(tBitmap)
    }
  }
  

и в функции takePhoto () вы можете удалить imageCapture.takePicture логику и заменить ее этим.

 Handler(Looper.getMainLooper()).postDelayed({
        captureView(binding.cameraWrapper, requireActivity().window) {
          // your new bitmap with overlay is here and you can save it to file just like any other bitmaps.
        }
      }, 500)