Skip to content

Commit

Permalink
Creating separate method to return redacted image and bboxes
Browse files Browse the repository at this point in the history
  • Loading branch information
niwilso committed Jul 13, 2023
1 parent 874c33d commit 805d9bb
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,16 @@ class DicomImageRedactorEngine(ImageRedactorEngine):
:param image_analyzer_engine: Engine which performs OCR + PII detection.
"""

def redact(
def redact_and_return_bbox(
self,
image: pydicom.dataset.FileDataset,
fill: str = "contrast",
padding_width: int = 25,
crop_ratio: float = 0.75,
return_bboxes: bool = False,
ocr_kwargs: Optional[dict] = None,
**text_analyzer_kwargs,
) -> Union[pydicom.dataset.FileDataset, Tuple[pydicom.dataset.FileDataset, dict]]:
"""Redact method to redact the given DICOM image.
"""Redact method to redact the given DICOM image and return redacted bboxes.
Please note, this method duplicates the image, creates a
new instance and manipulates it.
Expand All @@ -45,7 +44,6 @@ def redact(
:param padding_width: Padding width to use when running OCR.
:param crop_ratio: Portion of image to consider when selecting
most common pixel value as the background color value.
:param return_bboxes: True if we want to return boundings boxes.
:param ocr_kwargs: Additional params for OCR methods.
:param text_analyzer_kwargs: Additional values for the analyze method
in AnalyzerEngine.
Expand Down Expand Up @@ -96,13 +94,43 @@ def redact(
)
redacted_image = self._add_redact_box(instance, bboxes, crop_ratio, fill)

# Return just redacted image or image with bboxes
if return_bboxes:
results = [redacted_image, bboxes]
else:
results = redacted_image
return redacted_image, bboxes

def redact(
self,
image: pydicom.dataset.FileDataset,
fill: str = "contrast",
padding_width: int = 25,
crop_ratio: float = 0.75,
ocr_kwargs: Optional[dict] = None,
**text_analyzer_kwargs,
) -> pydicom.dataset.FileDataset:
"""Redact method to redact the given DICOM image.
Please note, this method duplicates the image, creates a
new instance and manipulates it.
:param image: Loaded DICOM instance including pixel data and metadata.
:param fill: Fill setting to use for redaction box ("contrast" or "background").
:param padding_width: Padding width to use when running OCR.
:param crop_ratio: Portion of image to consider when selecting
most common pixel value as the background color value.
:param ocr_kwargs: Additional params for OCR methods.
:param text_analyzer_kwargs: Additional values for the analyze method
in AnalyzerEngine.
:return: DICOM instance with redacted pixel data.
"""
redacted_image, _ = self.redact_and_return_bbox(
image = image,
fill = fill,
padding_width = padding_width,
crop_ratio = crop_ratio,
ocr_kwargs = ocr_kwargs,
**text_analyzer_kwargs
)

return results
return redacted_image

def redact_from_file(
self,
Expand Down
153 changes: 47 additions & 106 deletions presidio-image-redactor/tests/test_dicom_image_redactor_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,7 @@ def test_add_redact_box_happy_path(


# ------------------------------------------------------
# DicomImageRedactorEngine redact()
# DicomImageRedactorEngine redact_and_return_bbox()
# ------------------------------------------------------
@pytest.mark.parametrize(
"dcm_path",
Expand All @@ -1024,12 +1024,12 @@ def test_add_redact_box_happy_path(
(Path(TEST_DICOM_DIR_3, "3_ORIGINAL.DICOM")),
],
)
def test_DicomImageRedactorEngine_redact_happy_path(
def test_DicomImageRedactorEngine_redact_and_return_bbox(
mocker,
mock_engine: DicomImageRedactorEngine,
dcm_path: str,
):
"""Test happy path for DicomImageRedactorEngine redact()
"""Test happy path for DicomImageRedactorEngine redact_and_return_bbox()
Args:
mock_engine (DicomImageRedactorEngine): DicomImageRedactorEngine object.
Expand Down Expand Up @@ -1099,10 +1099,11 @@ def test_DicomImageRedactorEngine_redact_happy_path(
mock_engine = DicomImageRedactorEngine()

# Act
test_results = mock_engine.redact(test_image)
test_redacted_image, test_bboxes = mock_engine.redact_and_return_bbox(test_image)

# Assert
assert type(test_results) == pydicom.dataset.FileDataset
assert type(test_redacted_image) == pydicom.dataset.FileDataset
assert type(test_bboxes) == dict
assert mock_check_greyscale.call_count == 1
assert mock_rescale_dcm.call_count == 1
assert mock_save_pixel_array.call_count == 1
Expand All @@ -1116,6 +1117,39 @@ def test_DicomImageRedactorEngine_redact_happy_path(
assert mock_remove_bbox_padding.call_count == 1
assert mock_add_redact_box.call_count == 1

@pytest.mark.parametrize(
"image, expected_error_type",
[
(Path(TEST_DICOM_PARENT_DIR), "TypeError"),
("path_here", "TypeError"),
(np.random.randint(255, size=(64, 64)), "TypeError"),
(Image.fromarray(np.random.randint(255, size=(400, 400),dtype=np.uint8)), "TypeError"),
(pydicom.dcmread(Path(TEST_DICOM_PARENT_DIR, "0_ORIGINAL_no_pixels.dcm")), "AttributeError")
],
)
def test_DicomImageRedactorEngine_redact_and_return_bbox_exceptions(
mock_engine: DicomImageRedactorEngine,
image: T,
expected_error_type: str,
):
"""Test error handling of DicomImageRedactorEngine redact_and_return_bbox()
Args:
mock_engine (DicomImageRedactorEngine): DicomImageRedactorEngine object.
image (any): Input "image".
expected_error_type (str): Type of error we expect to be raised.
"""
with pytest.raises(Exception) as exc_info:
# Act
mock_engine.redact_and_return_bbox(image, "contrast", 25
)

# Assert
assert expected_error_type == exc_info.typename

# ------------------------------------------------------
# DicomImageRedactorEngine redact()
# ------------------------------------------------------
@pytest.mark.parametrize(
"dcm_path",
[
Expand All @@ -1127,12 +1161,12 @@ def test_DicomImageRedactorEngine_redact_happy_path(
(Path(TEST_DICOM_DIR_3, "3_ORIGINAL.DICOM")),
],
)
def test_DicomImageRedactorEngine_redact_return_bbox(
def test_DicomImageRedactorEngine_happy_path(
mocker,
mock_engine: DicomImageRedactorEngine,
dcm_path: str,
):
"""Test happy path for DicomImageRedactorEngine redact() when we return bboxes
"""Test happy path for DicomImageRedactorEngine redact()
Args:
mock_engine (DicomImageRedactorEngine): DicomImageRedactorEngine object.
Expand All @@ -1143,112 +1177,19 @@ def test_DicomImageRedactorEngine_redact_return_bbox(
# Arrange
test_image = pydicom.dcmread(dcm_path)

mock_check_greyscale = mocker.patch.object(
DicomImageRedactorEngine, "_check_if_greyscale", return_value=None
)
mock_rescale_dcm = mocker.patch.object(
DicomImageRedactorEngine, "_rescale_dcm_pixel_array", return_value=None
)
mock_save_pixel_array = mocker.patch.object(
DicomImageRedactorEngine, "_save_pixel_array_as_png", return_value=None
)
mock_image_open = mocker.patch(
"presidio_image_redactor.dicom_image_redactor_engine.Image.open",
return_value=None,
)
mock_add_padding = mocker.patch.object(
DicomImageRedactorEngine,
"_add_padding",
return_value=None,
)

mock_get_text_metadata = mocker.patch.object(
DicomImageRedactorEngine,
"_get_text_metadata",
return_value=[None, None, None],
)
mock_make_phi_list = mocker.patch.object(
DicomImageRedactorEngine,
"_make_phi_list",
return_value=None,
)

mock_pattern_recognizer = mocker.patch(
"presidio_image_redactor.dicom_image_redactor_engine.PatternRecognizer",
return_value=None,
)

mock_analyze = mocker.patch(
"presidio_image_redactor.dicom_image_redactor_engine.ImageAnalyzerEngine.analyze",
return_value=None,
)

mock_get_analyze_bbox = mocker.patch(
"presidio_image_redactor.image_redactor_engine.BboxProcessor.get_bboxes_from_analyzer_results",
return_value=None,
)

mock_remove_bbox_padding = mocker.patch(
"presidio_image_redactor.image_redactor_engine.BboxProcessor.remove_bbox_padding",
return_value={},
)

mock_add_redact_box = mocker.patch.object(
mock_redact_return_bbox = mocker.patch.object(
DicomImageRedactorEngine,
"_add_redact_box",
return_value=test_image,
"redact_and_return_bbox",
return_value=[test_image, None]
)

mock_engine = DicomImageRedactorEngine()

# Act
test_results = mock_engine.redact(test_image, return_bboxes=True)
test_redacted_image = mock_engine.redact(test_image)

# Assert
assert type(test_results[0]) == pydicom.dataset.FileDataset
assert type(test_results[1]) == dict
assert mock_check_greyscale.call_count == 1
assert mock_rescale_dcm.call_count == 1
assert mock_save_pixel_array.call_count == 1
assert mock_image_open.call_count == 1
assert mock_add_padding.call_count == 1
assert mock_get_text_metadata.call_count == 1
assert mock_make_phi_list.call_count == 1
assert mock_pattern_recognizer.call_count == 1
assert mock_analyze.call_count == 1
assert mock_get_analyze_bbox.call_count == 1
assert mock_remove_bbox_padding.call_count == 1
assert mock_add_redact_box.call_count == 1

@pytest.mark.parametrize(
"image, expected_error_type",
[
(Path(TEST_DICOM_PARENT_DIR), "TypeError"),
("path_here", "TypeError"),
(np.random.randint(255, size=(64, 64)), "TypeError"),
(Image.fromarray(np.random.randint(255, size=(400, 400),dtype=np.uint8)), "TypeError"),
(pydicom.dcmread(Path(TEST_DICOM_PARENT_DIR, "0_ORIGINAL_no_pixels.dcm")), "AttributeError")
],
)
def test_DicomImageRedactorEngine_redact_exceptions(
mock_engine: DicomImageRedactorEngine,
image: T,
expected_error_type: str,
):
"""Test error handling of DicomImageRedactorEngine _redact()
Args:
mock_engine (DicomImageRedactorEngine): DicomImageRedactorEngine object.
image (any): Input "image".
expected_error_type (str): Type of error we expect to be raised.
"""
with pytest.raises(Exception) as exc_info:
# Act
mock_engine.redact(image, "contrast", 25, False, ".", False
)

# Assert
assert expected_error_type == exc_info.typename
assert type(test_redacted_image) == pydicom.dataset.FileDataset
assert mock_redact_return_bbox.call_count == 1

# ------------------------------------------------------
# DicomImageRedactorEngine _save_bbox_json()
Expand Down

0 comments on commit 805d9bb

Please sign in to comment.