From 2fb273d40a52addd158bbb5f885e07615a7b1756 Mon Sep 17 00:00:00 2001 From: Dowon Date: Wed, 27 Mar 2024 00:42:41 +0900 Subject: [PATCH 1/6] chore: update pre-commit --- .pre-commit-config.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b86433..045827c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +ci: + autoupdate_branch: "dev" + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -13,7 +16,7 @@ repos: - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.3 + rev: v0.3.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 98043a8abd21736e3490fa733f75a451719e77fd Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 28 Mar 2024 19:37:56 +0900 Subject: [PATCH 2/6] test: update tests --- tests/conftest.py | 21 +--- tests/test_mask.py | 268 +++++++++++++++++++++++++++------------------ 2 files changed, 168 insertions(+), 121 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0c382bd..eb33c23 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,29 +1,18 @@ -from functools import cache - import pytest import requests from PIL import Image -@cache -def _sample_image(): - url = "https://i.imgur.com/E5OVXvn.png" +def get_image(url: str) -> Image.Image: resp = requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) return Image.open(resp.raw) -@cache -def _sample_image2(): - url = "https://i.imgur.com/px5UT7T.png" - resp = requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) - return Image.open(resp.raw) - - -@pytest.fixture() +@pytest.fixture(scope="session") def sample_image(): - return _sample_image() + return get_image("https://i.imgur.com/E5OVXvn.png") -@pytest.fixture() +@pytest.fixture(scope="session") def sample_image2(): - return _sample_image2() + return get_image("https://i.imgur.com/px5UT7T.png") diff --git a/tests/test_mask.py b/tests/test_mask.py index 19698d5..5d4dd20 100644 --- a/tests/test_mask.py +++ b/tests/test_mask.py @@ -3,7 +3,15 @@ import numpy as np import pytest from PIL import Image, ImageDraw -from adetailer.mask import dilate_erode, has_intersection, is_all_black, offset +from adetailer.mask import ( + bbox_area, + dilate_erode, + has_intersection, + is_all_black, + mask_invert, + mask_merge, + offset, +) def test_dilate_positive_value(): @@ -63,112 +71,162 @@ def test_offset(): assert np.array_equal(np.array(result), expect) -def test_is_all_black_1(): +class TestIsAllBlack: + def test_is_all_black_1(self): + img = Image.new("L", (10, 10), color="black") + assert is_all_black(img) + + draw = ImageDraw.Draw(img) + draw.rectangle((4, 4, 5, 5), fill="white") + assert not is_all_black(img) + + def test_is_all_black_2(self): + img = np.zeros((10, 10), dtype=np.uint8) + assert is_all_black(img) + + img[4:6, 4:6] = 255 + assert not is_all_black(img) + + def test_is_all_black_rgb_image_pil(self): + img = Image.new("RGB", (10, 10), color="red") + assert not is_all_black(img) + + img = Image.new("RGBA", (10, 10), color="red") + assert not is_all_black(img) + + def test_is_all_black_rgb_image_numpy(self): + img = np.full((10, 10, 4), 127, dtype=np.uint8) + with pytest.raises(cv2.error): + is_all_black(img) + + img = np.full((4, 10, 10), 0.5, dtype=np.float32) + with pytest.raises(cv2.error): + is_all_black(img) + + +class TestHasIntersection: + def test_has_intersection_1(self): + arr1 = np.array( + [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ] + ) + arr2 = arr1.copy() + assert not has_intersection(arr1, arr2) + + def test_has_intersection_2(self): + arr1 = np.array( + [ + [0, 0, 0, 0], + [0, 255, 255, 0], + [0, 255, 255, 0], + [0, 0, 0, 0], + ] + ) + arr2 = np.array( + [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 255, 255], + [0, 0, 255, 255], + ] + ) + assert has_intersection(arr1, arr2) + + arr3 = np.array( + [ + [255, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 255], + [0, 0, 255, 255], + ] + ) + assert not has_intersection(arr1, arr3) + + def test_has_intersection_3(self): + img1 = Image.new("L", (10, 10), color="black") + draw1 = ImageDraw.Draw(img1) + draw1.rectangle((3, 3, 5, 5), fill="white") + img2 = Image.new("L", (10, 10), color="black") + draw2 = ImageDraw.Draw(img2) + draw2.rectangle((6, 6, 8, 8), fill="white") + assert not has_intersection(img1, img2) + + img3 = Image.new("L", (10, 10), color="black") + draw3 = ImageDraw.Draw(img3) + draw3.rectangle((2, 2, 8, 8), fill="white") + assert has_intersection(img1, img3) + + def test_has_intersection_4(self): + img1 = Image.new("RGB", (10, 10), color="black") + draw1 = ImageDraw.Draw(img1) + draw1.rectangle((3, 3, 5, 5), fill="white") + img2 = Image.new("RGBA", (10, 10), color="black") + draw2 = ImageDraw.Draw(img2) + draw2.rectangle((2, 2, 8, 8), fill="white") + assert has_intersection(img1, img2) + + def test_has_intersection_5(self): + img1 = Image.new("RGB", (10, 10), color="black") + draw1 = ImageDraw.Draw(img1) + draw1.rectangle((4, 4, 5, 5), fill="white") + img2 = np.full((10, 10, 4), 255, dtype=np.uint8) + assert has_intersection(img1, img2) + + +def test_bbox_area(): + bbox = [0.0, 0.0, 10.0, 10.0] + assert bbox_area(bbox) == 100 + + +class TestMaskMerge: + def test_mask_merge(self): + img1 = Image.new("L", (10, 10), color="black") + draw1 = ImageDraw.Draw(img1) + draw1.rectangle((3, 3, 5, 5), fill="white") + + img2 = Image.new("L", (10, 10), color="black") + draw2 = ImageDraw.Draw(img2) + draw2.rectangle((6, 6, 8, 8), fill="white") + + merged = mask_merge([img1, img2]) + assert len(merged) == 1 + + expect = Image.new("L", (10, 10), color="black") + draw3 = ImageDraw.Draw(expect) + draw3.rectangle((3, 3, 5, 5), fill="white") + draw3.rectangle((6, 6, 8, 8), fill="white") + + assert np.array_equal(np.array(merged[0]), np.array(expect)) + + def test_merge_mask_different_size(self): + img1 = Image.new("L", (10, 10), color="black") + draw1 = ImageDraw.Draw(img1) + draw1.rectangle((3, 3, 5, 5), fill="white") + + img2 = Image.new("L", (20, 20), color="black") + draw2 = ImageDraw.Draw(img2) + draw2.rectangle((6, 6, 8, 8), fill="white") + + with pytest.raises( + cv2.error, match="-209:Sizes of input arguments do not match" + ): + mask_merge([img1, img2]) + + +def test_mask_invert(): img = Image.new("L", (10, 10), color="black") - assert is_all_black(img) - draw = ImageDraw.Draw(img) - draw.rectangle((4, 4, 5, 5), fill="white") - assert not is_all_black(img) + draw.rectangle((3, 3, 5, 5), fill="white") + inverted = mask_invert([img]) + assert len(inverted) == 1 -def test_is_all_black_2(): - img = np.zeros((10, 10), dtype=np.uint8) - assert is_all_black(img) + expect = Image.new("L", (10, 10), color="white") + draw = ImageDraw.Draw(expect) + draw.rectangle((3, 3, 5, 5), fill="black") - img[4:6, 4:6] = 255 - assert not is_all_black(img) - - -def test_is_all_black_rgb_image_pil(): - img = Image.new("RGB", (10, 10), color="red") - assert not is_all_black(img) - - img = Image.new("RGBA", (10, 10), color="red") - assert not is_all_black(img) - - -def test_is_all_black_rgb_image_numpy(): - img = np.full((10, 10, 4), 127, dtype=np.uint8) - with pytest.raises(cv2.error): - is_all_black(img) - - img = np.full((4, 10, 10), 0.5, dtype=np.float32) - with pytest.raises(cv2.error): - is_all_black(img) - - -def test_has_intersection_1(): - arr1 = np.array( - [ - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - ] - ) - arr2 = arr1.copy() - assert not has_intersection(arr1, arr2) - - -def test_has_intersection_2(): - arr1 = np.array( - [ - [0, 0, 0, 0], - [0, 255, 255, 0], - [0, 255, 255, 0], - [0, 0, 0, 0], - ] - ) - arr2 = np.array( - [ - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 255, 255], - [0, 0, 255, 255], - ] - ) - assert has_intersection(arr1, arr2) - - arr3 = np.array( - [ - [255, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 255], - [0, 0, 255, 255], - ] - ) - assert not has_intersection(arr1, arr3) - - -def test_has_intersection_3(): - img1 = Image.new("L", (10, 10), color="black") - draw1 = ImageDraw.Draw(img1) - draw1.rectangle((3, 3, 5, 5), fill="white") - img2 = Image.new("L", (10, 10), color="black") - draw2 = ImageDraw.Draw(img2) - draw2.rectangle((6, 6, 8, 8), fill="white") - assert not has_intersection(img1, img2) - - img3 = Image.new("L", (10, 10), color="black") - draw3 = ImageDraw.Draw(img3) - draw3.rectangle((2, 2, 8, 8), fill="white") - assert has_intersection(img1, img3) - - -def test_has_intersection_4(): - img1 = Image.new("RGB", (10, 10), color="black") - draw1 = ImageDraw.Draw(img1) - draw1.rectangle((3, 3, 5, 5), fill="white") - img2 = Image.new("RGBA", (10, 10), color="black") - draw2 = ImageDraw.Draw(img2) - draw2.rectangle((2, 2, 8, 8), fill="white") - assert has_intersection(img1, img2) - - -def test_has_intersection_5(): - img1 = Image.new("RGB", (10, 10), color="black") - draw1 = ImageDraw.Draw(img1) - draw1.rectangle((4, 4, 5, 5), fill="white") - img2 = np.full((10, 10, 4), 255, dtype=np.uint8) - assert has_intersection(img1, img2) + assert np.array_equal(np.array(inverted[0]), np.array(expect)) From 1ddace3d6540024167539d3cf4346a0d5397737e Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 28 Mar 2024 19:52:53 +0900 Subject: [PATCH 3/6] fix: resize img2img mask MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 타겟 이미지의 사이즈가 16의 배수가 아닐 때 --- scripts/!adetailer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/!adetailer.py b/scripts/!adetailer.py index 4af26d9..afe887b 100644 --- a/scripts/!adetailer.py +++ b/scripts/!adetailer.py @@ -660,6 +660,8 @@ class AfterDetailerScript(scripts.Script): def inpaint_mask_filter( img2img_mask: Image.Image, ad_mask: list[Image.Image] ) -> list[Image.Image]: + if ad_mask and img2img_mask.size != ad_mask[0].size: + img2img_mask = img2img_mask.resize(ad_mask[0].size, resample=images.LANCZOS) return [mask for mask in ad_mask if has_intersection(img2img_mask, mask)] @staticmethod From 5607fe63d2fd544365d7afeb6e192ff8945958f5 Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 28 Mar 2024 20:20:10 +0900 Subject: [PATCH 4/6] ci: pypi publish action --- .github/workflows/pypi.yml | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/pypi.yml diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..9136cc7 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,45 @@ +name: Publish to PyPI +on: + push: + tags: + - "v*" + +jobs: + test: + name: test + runs-on: macos-14 + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install . + pip install pytest + + - name: Run tests + run: pytest -v + + build: + name: build + runs-on: ubuntu-latest + permissions: + id-token: write + needs: [test] + + steps: + - uses: actions/checkout@v4 + + - name: Build wheel + run: pipx run build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From 742182fc5a618ae12510e7f6765d51c84071829c Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 28 Mar 2024 20:20:25 +0900 Subject: [PATCH 5/6] chore: update tools --- .github/workflows/stale.yml | 4 ++-- .vscode/extensions.json | 1 + Taskfile.yml | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6d3e128..e66d68e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,4 +1,4 @@ -name: 'Close stale issues and PRs' +name: Close stale issues and PRs on: schedule: - cron: '30 1 * * *' @@ -9,5 +9,5 @@ jobs: steps: - uses: actions/stale@v9 with: - days-before-stale: 23 + days-before-stale: 17 days-before-close: 3 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f18738d..5c50785 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ + "ms-python.vscode-pylance", "ms-python.black-formatter", "kevinrose.vsc-python-indent", "charliermarsh.ruff", diff --git a/Taskfile.yml b/Taskfile.yml index 70e0d35..355de2f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -24,4 +24,8 @@ tasks: update: cmds: - - "{{.PYTHON}} -m pip install -U ultralytics mediapipe ruff pre-commit black" + - "{{.PYTHON}} -m pip install -U ultralytics mediapipe ruff pre-commit black devtools pytest" + + update-torch: + cmds: + - "{{.PYTHON}} -m pip install -U torch torchvision torchaudio -f https://download.pytorch.org/whl/torch_stable.html" From b77dcd78ac3792d101827761cbf978cefc55179e Mon Sep 17 00:00:00 2001 From: Dowon Date: Thu, 28 Mar 2024 20:29:22 +0900 Subject: [PATCH 6/6] chore: v24.3.4 --- CHANGELOG.md | 5 +++++ adetailer/__version__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a2f73..8c6ad70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2024-03-28 + +- v24.3.4 +- 인페인트에서, 이미지 해상도가 16의 배수가 아닐 때 사이즈 불일치로 인한 opencv 에러 방지 + ## 2024-03-25 - v24.3.3 diff --git a/adetailer/__version__.py b/adetailer/__version__.py index 5f5467a..929b691 100644 --- a/adetailer/__version__.py +++ b/adetailer/__version__.py @@ -1 +1 @@ -__version__ = "24.3.3" +__version__ = "24.3.4"