화면 플래시(전면 플래시 또는 셀카 플래시라고도 함)는 어두운 환경에서 전면 카메라로 이미지를 캡처할 때 휴대전화의 화면 밝기를 활용하여 피사체를 비춥니다. 여러 네이티브 카메라 앱과 소셜 미디어 앱에서 사용할 수 있습니다. 대부분의 사용자가 셀카를 찍을 때 휴대전화를 충분히 가까이 들고 있기 때문에 이 방법이 효과적입니다.
그러나 개발자가 기능을 올바르게 구현하고 여러 기기에서 우수한 캡처 품질을 일관되게 유지하기는 어렵습니다. 이 가이드에서는 하위 수준 Android 카메라 프레임워크 API인 Camera2를 사용하여 이 기능을 올바르게 구현하는 방법을 보여줍니다.
일반 워크플로
이 기능을 올바르게 구현하려면 두 가지 주요 요소인 촬영 전 측정 시퀀스 (자동 노출 촬영 전)의 사용과 작업 시점이 중요합니다. 일반적인 워크플로는 그림 1에 나와 있습니다.
다음 단계는 화면 플래시 기능으로 이미지를 캡처해야 하는 경우에 사용됩니다.
- 기기 화면을 사용하여 사진을 찍기에 충분한 조명을 제공할 수 있는 화면 플래시에 필요한 UI 변경사항을 적용합니다. 일반적인 사용 사례의 경우 Google은 테스트에 사용된 다음과 같은 UI 변경사항을 제안합니다.
- 앱 화면이 흰색 오버레이로 덮여 있습니다.
- 화면 밝기가 극대화됩니다.
- 지원되는 경우 자동 노출 (AE) 모드를
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
로 설정합니다. CONTROL_AE_PRECAPTURE_TRIGGER
를 사용하여 사전 캡처 측정 시퀀스를 트리거합니다.자동 노출 (AE) 및 자동 화이트 밸런스 (AWB)가 수렴될 때까지 기다립니다.
수렴되면 앱의 일반적인 사진 캡처 흐름이 사용됩니다.
프레임워크에 캡처 요청을 전송합니다.
캡처 결과 수신을 기다립니다.
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
가 설정된 경우 AE 모드를 재설정합니다.화면 플래시의 UI 변경사항을 삭제합니다.
Camera2 샘플 코드
흰색 오버레이로 앱 화면을 가림
애플리케이션의 레이아웃 XML 파일에 뷰를 추가합니다. 뷰가 화면 플래시 캡처 중에 다른 모든 UI 요소 위에 표시될 만큼 충분히 높습니다. 기본적으로 표시되지 않으며 화면 플래시 UI 변경사항이 적용될 때만 표시됩니다.
다음 코드 샘플에서는 뷰의 예로 흰색 (#FFFFFF
)이 사용됩니다. 애플리케이션은 요구사항에 따라 색상을 선택하거나 사용자에게 여러 색상을 제공할 수 있습니다.
<View android:id="@+id/white_color_overlay" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" android:visibility="invisible" android:elevation="8dp" />
화면 밝기 극대화
Android 앱에서 화면 밝기를 변경하는 방법에는 여러 가지가 있습니다. 가장 직접적인 방법은 Activity Window 참조에서 screenBrightness WindowManager 매개변수를 변경하는 것입니다.
Kotlin
private var previousBrightness: Float = -1.0f private fun maximizeScreenBrightness() { activity?.window?.let { window -> window.attributes?.apply { previousBrightness = screenBrightness screenBrightness = 1f window.attributes = this } } } private fun restoreScreenBrightness() { activity?.window?.let { window -> window.attributes?.apply { screenBrightness = previousBrightness window.attributes = this } } }
자바
private float mPreviousBrightness = -1.0f; private void maximizeScreenBrightness() { if (getActivity() == null || getActivity().getWindow() == null) { return; } Window window = getActivity().getWindow(); WindowManager.LayoutParams attributes = window.getAttributes(); mPreviousBrightness = attributes.screenBrightness; attributes.screenBrightness = 1f; window.setAttributes(attributes); } private void restoreScreenBrightness() { if (getActivity() == null || getActivity().getWindow() == null) { return; } Window window = getActivity().getWindow(); WindowManager.LayoutParams attributes = window.getAttributes(); attributes.screenBrightness = mPreviousBrightness; window.setAttributes(attributes); }
AE 모드를 CONTROL_AE_MODE_ON_EXTERNAL_FLASH
로 설정
CONTROL_AE_MODE_ON_EXTERNAL_FLASH
는 API 수준 28 이상에서 사용할 수 있습니다.
그러나 이 AE 모드는 일부 기기에서만 사용할 수 있으므로 AE 모드를 사용할 수 있는지 확인하고 적절하게 값을 설정하세요. 사용 가능 여부를 확인하려면 CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
를 사용하세요.
Kotlin
private val characteristics: CameraCharacteristics by lazy { cameraManager.getCameraCharacteristics(cameraId) } @RequiresApi(Build.VERSION_CODES.P) private fun isExternalFlashAeModeAvailable() = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES) ?.contains(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) ?: false
자바
try { mCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId); } catch (CameraAccessException e) { e.printStackTrace(); } @RequiresApi(Build.VERSION_CODES.P) private boolean isExternalFlashAeModeAvailable() { int[] availableAeModes = mCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); for (int aeMode : availableAeModes) { if (aeMode == CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) { return true; } } return false; }
애플리케이션에 반복 캡처 요청이 설정된 경우 (미리보기에 필요함) AE 모드를 반복 요청으로 설정해야 합니다. 그러지 않으면 다음 반복 캡처에서 기본 모드 또는 다른 사용자가 설정한 AE 모드로 재정의될 수 있습니다. 이 경우 카메라가 외부 플래시 AE 모드에서 일반적으로 실행하는 모든 작업을 실행할 시간이 충분하지 않을 수 있습니다.
카메라가 AE 모드 업데이트 요청을 완전히 처리할 수 있도록 반복 캡처 콜백에서 캡처 결과를 확인하고 결과에서 AE 모드가 업데이트될 때까지 기다립니다.
AE 모드가 업데이트될 때까지 기다릴 수 있는 캡처 콜백
다음 코드 스니펫은 이를 실행하는 방법을 보여줍니다.
Kotlin
private val repeatingCaptureCallback = object : CameraCaptureSession.CaptureCallback() { private var targetAeMode: Int? = null private var aeModeUpdateDeferred: CompletableDeferred? = null suspend fun awaitAeModeUpdate(targetAeMode: Int) { this.targetAeMode = targetAeMode aeModeUpdateDeferred = CompletableDeferred() // Makes the current coroutine wait until aeModeUpdateDeferred is completed. It is // completed once targetAeMode is found in the following capture callbacks aeModeUpdateDeferred?.await() } private fun process(result: CaptureResult) { // Checks if AE mode is updated and completes any awaiting Deferred aeModeUpdateDeferred?.let { val aeMode = result[CaptureResult.CONTROL_AE_MODE] if (aeMode == targetAeMode) { it.complete(Unit) } } } override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) process(result) } }
자바
static class AwaitingCaptureCallback extends CameraCaptureSession.CaptureCallback { private int mTargetAeMode; private CountDownLatch mAeModeUpdateLatch = null; public void awaitAeModeUpdate(int targetAeMode) { mTargetAeMode = targetAeMode; mAeModeUpdateLatch = new CountDownLatch(1); // Makes the current thread wait until mAeModeUpdateLatch is released, it will be // released once targetAeMode is found in the capture callbacks below try { mAeModeUpdateLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } private void process(CaptureResult result) { // Checks if AE mode is updated and decrements the count of any awaiting latch if (mAeModeUpdateLatch != null) { int aeMode = result.get(CaptureResult.CONTROL_AE_MODE); if (aeMode == mTargetAeMode) { mAeModeUpdateLatch.countDown(); } } } @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); process(result); } } private final AwaitingCaptureCallback mRepeatingCaptureCallback = new AwaitingCaptureCallback();
AE 모드를 사용 설정하거나 중지하기 위한 반복 요청 설정
캡처 콜백이 준비된 상태에서 다음 코드 샘플은 반복 요청을 설정하는 방법을 보여줍니다.
Kotlin
/** [HandlerThread] where all camera operations run */ private val cameraThread = HandlerThread("CameraThread").apply { start() } /** [Handler] corresponding to [cameraThread] */ private val cameraHandler = Handler(cameraThread.looper) private suspend fun enableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { session.setRepeatingRequest( camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) set( CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH ) }.build(), repeatingCaptureCallback, cameraHandler ) // Wait for the request to be processed by camera repeatingCaptureCallback.awaitAeModeUpdate(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH) } } private fun disableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { session.setRepeatingRequest( camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) }.build(), repeatingCaptureCallback, cameraHandler ) } }
자바
private void setupCameraThread() { // HandlerThread where all camera operations run HandlerThread cameraThread = new HandlerThread("CameraThread"); cameraThread.start(); // Handler corresponding to cameraThread mCameraHandler = new Handler(cameraThread.getLooper()); } private void enableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { try { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH); mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler); } catch (CameraAccessException e) { e.printStackTrace(); } // Wait for the request to be processed by camera mRepeatingCaptureCallback.awaitAeModeUpdate(CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH); } } private void disableExternalFlashAeMode() { if (Build.VERSION.SDK_INT >= 28 && isExternalFlashAeModeAvailable()) { try { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } }
사전 캡처 시퀀스 트리거
촬영 전 측정 시퀀스를 트리거하려면 CONTROL_AE_PRECAPTURE_TRIGGER_START
값을 요청에 설정하여 CaptureRequest
를 제출하면 됩니다. 요청이 처리될 때까지 기다린 후 AE 및 AWB가 수렴할 때까지 기다려야 합니다.
단일 캡처 요청으로 사전 캡처가 트리거되지만 AE 및 AWB 수렴을 기다리려면 더 많은 복잡성이 필요합니다. 반복 요청으로 설정된 캡처 콜백을 사용하여 AE 상태와 AWB 상태를 추적할 수 있습니다.
동일한 반복 콜백을 업데이트하면 코드를 단순하게 만들 수 있습니다. 애플리케이션에는 카메라를 설정하는 동안 반복 요청을 설정하는 미리보기가 필요한 경우가 많습니다. 따라서 반복 캡처 콜백을 초기 반복 요청에 한 번 설정한 후 결과 확인 및 대기 목적으로 재사용할 수 있습니다.
수렴을 기다리기 위해 캡처 콜백 코드 업데이트
반복 캡처 콜백을 업데이트하려면 다음 코드 스니펫을 사용하세요.
Kotlin
private val repeatingCaptureCallback = object : CameraCaptureSession.CaptureCallback() { private var targetAeMode: Int? = null private var aeModeUpdateDeferred: CompletableDeferred? = null private var convergenceDeferred: CompletableDeferred? = null suspend fun awaitAeModeUpdate(targetAeMode: Int) { this.targetAeMode = targetAeMode aeModeUpdateDeferred = CompletableDeferred() // Makes the current coroutine wait until aeModeUpdateDeferred is completed. It is // completed once targetAeMode is found in the following capture callbacks aeModeUpdateDeferred?.await() } suspend fun awaitAeAwbConvergence() { convergenceDeferred = CompletableDeferred() // Makes the current coroutine wait until convergenceDeferred is completed, it will be // completed once both AE & AWB are reported as converged in the capture callbacks below convergenceDeferred?.await() } private fun process(result: CaptureResult) { // Checks if AE mode is updated and completes any awaiting Deferred aeModeUpdateDeferred?.let { val aeMode = result[CaptureResult.CONTROL_AE_MODE] if (aeMode == targetAeMode) { it.complete(Unit) } } // Checks for convergence and completes any awaiting Deferred convergenceDeferred?.let { val aeState = result[CaptureResult.CONTROL_AE_STATE] val awbState = result[CaptureResult.CONTROL_AWB_STATE] val isAeReady = ( aeState == null // May be null in some devices (e.g. legacy camera HW level) || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED ) val isAwbReady = ( awbState == null // May be null in some devices (e.g. legacy camera HW level) || awbState == CaptureResult.CONTROL_AWB_STATE_CONVERGED ) if (isAeReady && isAwbReady) { // if any non-null convergenceDeferred is set, complete it it.complete(Unit) } } } override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) process(result) } }
자바
static class AwaitingCaptureCallback extends CameraCaptureSession.CaptureCallback { private int mTargetAeMode; private CountDownLatch mAeModeUpdateLatch = null; private CountDownLatch mConvergenceLatch = null; public void awaitAeModeUpdate(int targetAeMode) { mTargetAeMode = targetAeMode; mAeModeUpdateLatch = new CountDownLatch(1); // Makes the current thread wait until mAeModeUpdateLatch is released, it will be // released once targetAeMode is found in the capture callbacks below try { mAeModeUpdateLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } public void awaitAeAwbConvergence() { mConvergenceLatch = new CountDownLatch(1); // Makes the current coroutine wait until mConvergenceLatch is released, it will be // released once both AE & AWB are reported as converged in the capture callbacks below try { mConvergenceLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } private void process(CaptureResult result) { // Checks if AE mode is updated and decrements the count of any awaiting latch if (mAeModeUpdateLatch != null) { int aeMode = result.get(CaptureResult.CONTROL_AE_MODE); if (aeMode == mTargetAeMode) { mAeModeUpdateLatch.countDown(); } } // Checks for convergence and decrements the count of any awaiting latch if (mConvergenceLatch != null) { Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); Integer awbState = result.get(CaptureResult.CONTROL_AWB_STATE); boolean isAeReady = ( aeState == null // May be null in some devices (e.g. legacy camera HW level) || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED ); boolean isAwbReady = ( awbState == null // May be null in some devices (e.g. legacy camera HW level) || awbState == CaptureResult.CONTROL_AWB_STATE_CONVERGED ); if (isAeReady && isAwbReady) { mConvergenceLatch.countDown(); mConvergenceLatch = null; } } } @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); process(result); } }
카메라 설정 중에 반복 요청으로 콜백 설정
다음 코드 샘플을 사용하면 초기화 중에 반복 요청에 콜백을 설정할 수 있습니다.
Kotlin
// Open the selected camera camera = openCamera(cameraManager, cameraId, cameraHandler) // Creates list of Surfaces where the camera will output frames val targets = listOf(previewSurface, imageReaderSurface) // Start a capture session using our open camera and list of Surfaces where frames will go session = createCameraCaptureSession(camera, targets, cameraHandler) val captureRequest = camera.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(previewSurface) } // This will keep sending the capture request as frequently as possible until the // session is torn down or session.stopRepeating() is called session.setRepeatingRequest(captureRequest.build(), repeatingCaptureCallback, cameraHandler)
자바
// Open the selected camera mCamera = openCamera(mCameraManager, mCameraId, mCameraHandler); // Creates list of Surfaces where the camera will output frames Listtargets = new ArrayList<>(Arrays.asList(mPreviewSurface, mImageReaderSurface)); // Start a capture session using our open camera and list of Surfaces where frames will go mSession = createCaptureSession(mCamera, targets, mCameraHandler); try { CaptureRequest.Builder requestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); // This will keep sending the capture request as frequently as possible until the // session is torn down or session.stopRepeating() is called mSession.setRepeatingRequest(requestBuilder.build(), mRepeatingCaptureCallback, mCameraHandler); } catch (CameraAccessException e) { e.printStackTrace(); }
사전 캡처 시퀀스 트리거 및 대기
콜백이 설정되면 사전 캡처 시퀀스가 트리거되고 기다리는 데 다음 코드 샘플을 사용할 수 있습니다.
Kotlin
private suspend fun runPrecaptureSequence() { // Creates a new capture request with CONTROL_AE_PRECAPTURE_TRIGGER_START val captureRequest = session.device.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW ).apply { addTarget(previewSurface) set( CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START ) } val precaptureDeferred = CompletableDeferred() session.capture(captureRequest.build(), object: CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { // Waiting for this callback ensures the precapture request has been processed precaptureDeferred.complete(Unit) } }, cameraHandler) precaptureDeferred.await() // Precapture trigger request has been processed, we can wait for AE & AWB convergence now repeatingCaptureCallback.awaitAeAwbConvergence() }
자바
private void runPrecaptureSequence() { // Creates a new capture request with CONTROL_AE_PRECAPTURE_TRIGGER_START try { CaptureRequest.Builder requestBuilder = mSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); requestBuilder.addTarget(mPreviewSurface); requestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); CountDownLatch precaptureLatch = new CountDownLatch(1); mSession.capture(requestBuilder.build(), new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { Log.d(TAG, "CONTROL_AE_PRECAPTURE_TRIGGER_START processed"); // Waiting for this callback ensures the precapture request has been processed precaptureLatch.countDown(); } }, mCameraHandler); precaptureLatch.await(); // Precapture trigger request has been processed, we can wait for AE & AWB convergence now mRepeatingCaptureCallback.awaitAeAwbConvergence(); } catch (CameraAccessException | InterruptedException e) { e.printStackTrace(); } }
모든 것을 연결
모든 주요 구성요소가 준비되면 사용자가 캡처 버튼을 클릭하여 사진을 찍을 때와 같이 사진을 찍어야 할 때마다 모든 단계가 이전 설명 및 코드 샘플에 명시된 순서대로 실행될 수 있습니다.
Kotlin
// User clicks captureButton to take picture captureButton.setOnClickListener { v -> // Apply the screen flash related UI changes whiteColorOverlayView.visibility = View.VISIBLE maximizeScreenBrightness() // Perform I/O heavy operations in a different scope lifecycleScope.launch(Dispatchers.IO) { // Enable external flash AE mode and wait for it to be processed enableExternalFlashAeMode() // Run precapture sequence and wait for it to complete runPrecaptureSequence() // Start taking picture and wait for it to complete takePhoto() disableExternalFlashAeMode() v.post { // Clear the screen flash related UI changes restoreScreenBrightness() whiteColorOverlayView.visibility = View.INVISIBLE } } }
자바
// User clicks captureButton to take picture mCaptureButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Apply the screen flash related UI changes mWhiteColorOverlayView.setVisibility(View.VISIBLE); maximizeScreenBrightness(); // Perform heavy operations in a different thread Executors.newSingleThreadExecutor().execute(() -> { // Enable external flash AE mode and wait for it to be processed enableExternalFlashAeMode(); // Run precapture sequence and wait for it to complete runPrecaptureSequence(); // Start taking picture and wait for it to complete takePhoto(); disableExternalFlashAeMode(); v.post(() -> { // Clear the screen flash related UI changes restoreScreenBrightness(); mWhiteColorOverlayView.setVisibility(View.INVISIBLE); }); }); } });
샘플 사진
다음 예에서 화면 플래시가 잘못 구현되면 어떻게 되는지, 그리고 올바르게 구현되면 어떻게 되는지 확인할 수 있습니다.
잘못된 경우
화면 플래시가 제대로 구현되지 않으면 여러 캡처, 기기, 조명 조건에서 일관되지 않은 결과가 나타납니다. 촬영된 이미지에 노출이나 색조 문제가 있는 경우가 많습니다. 일부 기기의 경우 이러한 유형의 버그는 완전히 어두운 환경이 아닌 저조도 환경과 같은 특정 조명 조건에서 더 명확하게 나타납니다.
다음 표는 이러한 문제의 예를 보여줍니다. 이 광원은 CameraX 실험실 인프라에서 촬영되었으며 광원은 따뜻한 흰색으로 유지됩니다. 이 따뜻한 백색광원에서는 파란색 색조가 광원의 부작용이 아니라 실제 문제임을 알 수 있습니다.
환경 | 노출 부족 | 과다 노출 | 색조 |
---|---|---|---|
어두운 환경 (휴대전화 이외의 광원 없음) | |||
저조도 (추가 광원 3럭스 추가) |
올바르게 수행하는 경우
동일한 기기와 조건에 표준 구현을 사용하면 다음 표에서 결과를 확인할 수 있습니다.
환경 | 노출 부족 (해결됨) | 노출 오버 (해결됨) | 색 번짐 (고정) |
---|---|---|---|
어두운 환경 (휴대전화 이외의 광원 없음) | |||
저조도 (추가 약 3lux 광원) |
표준 구현을 사용하면 이미지 품질이 크게 개선되는 것을 확인할 수 있습니다.