Skip to content

Commit

Permalink
Merge pull request #42 from DigitalSlideArchive/initial-rule-algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
naglepuff committed Feb 1, 2023
2 parents 37abc90 + bc7f642 commit 7ea2999
Show file tree
Hide file tree
Showing 9 changed files with 410 additions and 135 deletions.
74 changes: 0 additions & 74 deletions imagedephi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,74 +0,0 @@
from __future__ import annotations

from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any

import click
import tifftools
import tifftools.constants

if TYPE_CHECKING:
from tifftools.tifftools import IFD, TiffInfo


class RedactMethod(Enum):
REPLACE = 1
DELETE = 2


def get_tags_to_redact() -> dict[int, dict[str, Any]]:
return {
270: {
"id": 270,
"name": "ImageDescription",
"method": RedactMethod.REPLACE,
"replace_value": "Redacted by ImageDePHI",
}
}


def redact_one_tag(ifd: IFD, tag: tifftools.TiffTag, redact_instructions: dict[str, Any]) -> None:
if redact_instructions["method"] == RedactMethod.REPLACE:
ifd["tags"][tag.value]["data"] = redact_instructions["replace_value"]
elif redact_instructions["method"] == RedactMethod.DELETE:
del ifd["tags"][tag.value]


def redact_tiff_tags(ifds: list[IFD], tags_to_redact: dict[int, dict[str, Any]]) -> None:
for ifd in ifds:
for tag_id, tag_info in sorted(ifd["tags"].items()):
tag: tifftools.TiffTag = tifftools.constants.get_or_create_tag(
tag_id,
datatype=tifftools.Datatype[tag_info["datatype"]],
)
if not tag.isIFD():
if tag.value in tags_to_redact:
redact_one_tag(ifd, tag, tags_to_redact[tag.value])
else:
# tag_info['ifds'] contains a list of lists
# see tifftools.read_tiff
for sub_ifds in tag_info.get("ifds", []):
redact_tiff_tags(sub_ifds, tags_to_redact)


def redact_one_image(tiff_info: TiffInfo, output_path: Path) -> None:
ifds = tiff_info["ifds"]
tags_to_redact = get_tags_to_redact()
redact_tiff_tags(ifds, tags_to_redact)
tifftools.write_tiff(tiff_info, output_path)


def get_output_path(file_path: Path, output_dir: Path) -> Path:
return output_dir / f"REDACTED_{file_path.name}"


def redact_images(image_dir: Path, output_dir: Path) -> None:
for child in image_dir.iterdir():
try:
tiff_info: TiffInfo = tifftools.read_tiff(child)
except tifftools.TifftoolsError:
click.echo(f"Could not open {child.name} as a tiff. Skipping...")
continue
click.echo(f"Redacting {child.name}...")
redact_one_image(tiff_info, get_output_path(child, output_dir))
84 changes: 84 additions & 0 deletions imagedephi/base_rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: Base rules
description: These are the default rules for each potential piece of redactable material (metadata, image) to be used as a fallback if there are no user-defined rules for a piece of redactable material.
rules:
# For images that strictly adhere to the tiff standards e.g. svs files
tiff:
- tag_name: NewSubfileType
method: keep
type: metadata
- tag_name: ImageWidth
method: keep
type: metadata
- tag_name: ImageLength
method: keep
type: metadata
- tag_name: BitsPerSample
method: keep
type: metadata
- tag_name: Compression
method: keep
type: metadata
- tag_name: Photometric
method: keep
type: metadata
- tag_name: ImageDescription
method: delete
type: metadata
- tag_name: Orientation
method: keep
type: metadata
- tag_name: SamplesPerPixel
method: keep
type: metadata
- tag_name: XResolution
method: keep
type: metadata
- tag_name: YResolution
method: keep
type: metadata
- tag_name: PlanarConfig
method: keep
type: metadata
- tag_name: ResolutionUnit
method: keep
type: metadata
- tag_name: TileWidth
method: keep
type: metadata
- tag_name: TileHeight
method: keep
type: metadata
- tag_name: TileOffsets
method: keep
type: metadata
- tag_name: TileByteCounts
method: keep
type: metadata
- tag_name: SampleFormat
method: keep
type: metadata
- tag_name: JPEGTables
method: keep
type: metadata
- tag_name: YCbCrSubsampling
method: keep
type: metadata
- tag_name: ImageDepth
method: keep
type: metadata
- tag_name: ICCProfile
method: keep
type: metadata
- tag_name: StripOffsets
method: keep
type: metadata
- tag_name: RowsPerStrip
method: keep
type: metadata
- tag_name: StripByteCounts
method: keep
type: metadata
- tag_name: Predictor
method: keep
type: metadata
46 changes: 38 additions & 8 deletions imagedephi/main.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,65 @@
from __future__ import annotations

import asyncio
from dataclasses import dataclass
from pathlib import Path
from typing import TextIO
import webbrowser

import click
from hypercorn import Config
from hypercorn.asyncio import serve
import yaml

from imagedephi.async_utils import run_coroutine, wait_for_port
from imagedephi.gui import app, shutdown_event
from imagedephi.redact import redact_images
from imagedephi.redact import RuleSource, build_ruleset, redact_images, show_redaction_plan
from imagedephi.rules import RuleSet


@dataclass
class ImagedephiContext:
override_rule_set: RuleSet | None = None


@click.group
def imagedephi() -> None:
@click.option(
"-r",
"--override-rules",
type=click.File("r"),
help="Specify user-defined rules to override defaults",
)
@click.pass_context
def imagedephi(ctx: click.Context, override_rules: TextIO | None) -> None:
"""Redact microscopy whole slide images."""
pass
obj = ImagedephiContext()
# Store separately, to preserve the type of "obj"
ctx.obj = obj

if override_rules:
obj.override_rule_set = build_ruleset(yaml.safe_load(override_rules), RuleSource.OVERRIDE)


@imagedephi.command
@click.argument(
"input-dir", type=click.Path(exists=True, file_okay=False, readable=True, path_type=Path)
)
@click.argument(
"output-dir", type=click.Path(exists=True, file_okay=False, writable=True, path_type=Path)
"output-dir",
type=click.Path(exists=True, file_okay=False, readable=True, writable=True, path_type=Path),
)
def run(input_dir: Path, output_dir: Path) -> None:
"""Run in CLI-only mode."""
redact_images(input_dir, output_dir)
click.echo("Done!")
@click.pass_obj
def run(obj: ImagedephiContext, input_dir: Path, output_dir: Path):
"""Redact images in a folder according to given rule sets."""
redact_images(input_dir, output_dir, obj.override_rule_set)


@imagedephi.command
@click.argument("image", type=click.Path())
@click.pass_obj
def plan(obj: ImagedephiContext, image: click.Path) -> None:
"""Print the redaction plan for a given image and rules."""
show_redaction_plan(image, obj.override_rule_set)


@imagedephi.command
Expand Down

0 comments on commit 7ea2999

Please sign in to comment.