diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index ec70354..ae55fc2 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -21,9 +21,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies @@ -33,7 +33,7 @@ jobs: - name: Build package run: python -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6206838 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +# this file is *not* meant to cover or endorse the use of GitHub Actions, but rather to +# help test this project + +name: Test + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python: ['3.9', '3.10', '3.11', '3.12'] + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install test dependencies + run: python -m pip install -U tox + - name: Test + run: python -m tox -e py diff --git a/.gitignore b/.gitignore index 13d936b..8f42c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,5 @@ dmypy.json .pyre/ images/ -temp* \ No newline at end of file +temp* +excluded/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cefcd89 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,175 @@ +# Changelogs + +## v1.2.3 + +
+ Click here to show more. + +In this version, we have made the following changes: + +1. 🧬 **CHANGE!**: We change the GUI mode to **optional**. + * Now, you can install the GUI mode by running: + * ```bash + pip install skin-tone-classifier[all] --upgrade + ``` + * It will support both the **CLI** mode and the **GUI** mode. + * If you don't specify the `[all]` option, the app will install the CLI mode only. +2. 🧬 **CHANGE!**: [For developer]. We base the project to `project.toml` instead of `setup.py`. + + +
+ +## v1.2.0 + +
+ Click here to show more. + +In this version, we have made the following changes: + +1. ✨ **NEW!**: We add a GUI version of `stone` for users who are not familiar with the command line interface. + * You can use the config GUI of `stone` to process the images. + * See more information at [here](#use-stone-in-a-gui). +2. ✨ **NEW!**: We add new **patterns** in the `-l` (or `--labels`) option to set the skin tone labels. + * Now, you can use the following patterns to set the skin tone labels: + * **Default value**: the uppercase alphabet list leading by the image type (`C` for `color`; `B` + for `Black&White`). + * Specify the labels directly using _a space_ as delimiters, e.g., `-l A B C D E` or `-l 1 2 3 4 5`. + * Specify the range of labels using _a hyphen_ as delimiters, e.g., + * `-l A-E` (equivalent to `-l A B C D E`); + * `-l A-E-2` (equivalent to `-l A C E`); + * `-l 1-5` (equivalent to `-l 1 2 3 4 5`); + * `-l 1-10-3` (equivalent to `-l 1 4 7 10`); + * **NB**: The number of skin tone labels should be equal to the number of colors in the palette. + +
+ +## v1.1.2 + +
+ Click here to show more. + +In this version, we have made the following changes: + +1. 🐛 **FIX!**: We fixed a bug where the app will crash when using the `-bw` option. + Error message: `cannot reshape array of size 62500 into shape (3)`. +2. 🐛 **FIX!**: We fixed a bug where the app may identify the image type as `color` when using the `-bw` option. + +
+ +## v1.1.1 + +
+ Click here to show more. + +In this version, we have made the following changes: + +1. ✨ **NEW!**: We add the `-v` (or `--version`) option to show the version number. +2. ✨ **NEW!**: We add the `-r` (or `--recursive`) option to **enable** recursive search for images. + * For example, `stone -i ./path/to/images/ -r` will search all images in the `./path/to/images/` directory **and its + subdirectories**. + * `stone -i ./path/to/images/` will only search images in the `./path/to/images/` directory. +3. 🐛 **FIX!**: We fixed a bug where the app cannot correctly identify the current folder if `-i` option is not + specified. + +
+ +## v1.1.0 + +
+ Click here to show more. + +In this version, we have made the following changes: + +1. ✨ **NEW!**: Now, `stone` can not only be run on **the command line**, but can also be **imported** into other + projects for use. Check [this](#9-used-as-a-library-by-importing-into-other-projects) for more details. + * We expose the `process` and `show` functions in the `stone` package. +2. ✨ **NEW!**: We add `URL` support for the input images. + * Now, you can specify the input image as a URL, e.g., `https://example.com/images/pic.jpg`. Of course, you can mix + the URLs and local filenames. +3. ✨ **NEW!**: We add **recursive search** support for the input images. + * Now, when you specify the input image as a directory, e.g., `./path/to/images/`. + The app will search all images in the directory recursively. +4. 🧬 **CHANGE!**: We change the column header in `result.csv`: + * `prop` => `percent` + * `PERLA` => `tone label` +5. 🐛 **FIX!**: We fixed a bug where the app would not correctly sort files that did not contain numbers in their + filenames. + +
+ +## v1.0.1 + +
+ Click here to show more. + +1. 👋 **BYE**: We have removed the function to pop up a resulting window when processing a **single** image. + + * It can raise an error when running the app in a **web browser** environment, e.g., Jupyter Notebook or Google + Colab. + * If you want to see the processed image, please use the `-d` option to store the report image in the `./debug` + folder. + +
+ +## v1.0.0 + +
+ Click here to show more. + +🎉**We have officially released the 1.0.0 version of the library!** In this version, we have made the following changes: + +1. ✨ **NEW!**: We add the `threshold` parameter to control the minimum percentage of required face areas (Defaults to + 0.15). + * In previous versions, the library could incorrectly identify non-face areas as faces, such as shirts, collars, + necks, etc. + In order to improve its accuracy, the new version will further calculate the proportion of skin in the recognized + area + after recognizing the facial area. If it is less than the `threshold` value, the recognition area will be ignored. + (While it's still not perfect, it's an improvement over what it was before.) +2. ✨ **NEW!**: Now, we will back up the previous results if it already exists. + The backup file will be named as `result_bak_.csv`. +3. 🐛 **FIX!**: We fix the bug that the `image_type` option does not work in the previous version. +4. 🐛 **FIX!**: We fix the bug that the library will create an empty `log` folder when checking the help information by + running `stone -h`. + +
+ +## v0.2.0 + +
+ Click here to show more. + +In this version, we have made the following changes: + +1. ✨ **NEW!**: Now we support skin tone classification for **black and white** images. + * In this case, the app will use different skin tone palettes for color images and black/white images. + * We use a new parameter `-t` or `--image_type` to specify the type of the input image. + It can be `color`, `bw` or `auto`(default). + `auto` will let the app automatically detect whether the input is color or black/white image. + * We use a new parameter `-bw` or `--black_white` to specify whether to convert the input to black/white image. + If so, the app will convert the input to black/white image and then classify the skin tones based on the + black/white palette. + + For example: +
+ Processing color image + Processing black/white image +
+ +2. ✨ **NEW!**: Now we support **multiprocessing** for processing the images. It will largely speed up the processing. + * The number of processes is set to the number of CPU cores by default. + * You can specify the number of processes by `--n_workers` parameter. +3. 🧬 **CHANGE!**: We add more details in the report image to facilitate the debugging, as shown above. + * We add the face id in the report image. + * We add the effective face or skin area in the report image. In this case, the other areas are blurred. +4. 🧬 **CHANGE!**: Now, we save the report images into different folders based on their `image_type` (color or + black/white) and the number of detected faces. + * For example, if the input image is **color** and there are **2 faces** detected, the report image will be saved + in `./debug/color/faces_2/` folder. + * If the input image is **black/white** and no face has been detected, the report image will be saved + in `./debug/bw/faces_0/` folder. + * You can easily to tune the parameters and rerun the app based on the report images in the corresponding folder. +5. 🐛 **FIX!**: We fix the bug that the app will crash when the input image has dimensionality errors. + * Now, the app won't crash and will report the error message in `./result.csv`. + +
\ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index bda61f6..51295e0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,8 @@ include LICENSE include MANIFEST.in recursive-include src/stone/ui * recursive-exclude * *.py[co] +exclude .idea/* +exclude CHANGELOG.md +exclude _config.yml +exclude docs/* +exclude requirements.txt diff --git a/README.md b/README.md index fd955cd..5dd1fe5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@
stone logo + model illustration
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/skin-tone-classifier) @@ -18,6 +19,8 @@ The detected skin tones are then classified into the specified color categories. The library finally generates results to report the detected faces (if any), dominant skin tones and color categories. +Check out the [Changelog](https://github.com/ChenglongMa/SkinToneClassifier/blob/main/CHANGELOG.md) for the latest updates. + *If you find this project helpful, please consider [giving it a star](https://github.com/ChenglongMa/SkinToneClassifier)* ⭐. *It would be a great encouragement for me!* @@ -36,6 +39,8 @@ for me!* - [4. Use `stone` in Python scripts](#4-use-stone-in-python-scripts) - [Installation](#installation) - [Install from pip](#install-from-pip) + - [Install the CLI mode only](#install-the-cli-mode-only) + - [Install the CLI mode and the GUI mode](#install-the-cli-mode-and-the-gui-mode) - [Install from source](#install-from-source) - [HOW TO USE](#how-to-use) - [Quick Start](#quick-start) @@ -54,15 +59,6 @@ for me!* - [8. Tune parameters of face detection](#8-tune-parameters-of-face-detection) - [9. Multiprocessing settings](#9-multiprocessing-settings) - [10. Used as a library by importing into other projects](#10-used-as-a-library-by-importing-into-other-projects) -- [Changelogs](#changelogs) - - [v1.2.0](#v120) - - [v1.1.3](#v113) - - [v1.1.2](#v112) - - [v1.1.1](#v111) - - [v1.1.0](#v110) - - [v1.0.1](#v101) - - [v1.0.0](#v100) - - [v0.2.0](#v020) - [Citation](#citation) - [Contributing](#contributing) - [Disclaimer](#disclaimer) @@ -110,12 +106,30 @@ _More videos are coming soon..._ # Installation +> [!TIP] +> +> Since v1.2.3, we have made the GUI mode **optional**. +> + + ## Install from pip +### Install the CLI mode only + ```shell pip install skin-tone-classifier --upgrade ``` +It is useful for users who want to use this library in non-GUI environments, e.g., servers or [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1k-cryEZ9PInJRXWIi17ib66ufYV2Ikwe?usp=sharing). + +### Install the CLI mode and the GUI mode + +```shell +pip install skin-tone-classifier[all] --upgrade +``` + +It is useful for users who are not familiar with the command line interface and want to use the GUI mode. + ## Install from source ```shell @@ -141,7 +155,6 @@ pip install -e . --verbose > You can combine the following documents, [the video tutorials above](#video-tutorials) > and the running examples [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1k-cryEZ9PInJRXWIi17ib66ufYV2Ikwe?usp=sharing) > to understand the usage of this library more intuitively. -> > ## Quick Start @@ -166,12 +179,21 @@ Hopefully, this can make it easier for you to use `stone` 🍻! > [!TIP] > -> It is recommended to install v1.2.1, which supports Python 3.9+. +> 1. It is recommended to install v1.2.3+, which supports Python 3.9+. > -> If you have installed v1.2.0, please upgrade to v1.2.1 by running +> If you have installed v1.2.0, please upgrade to v1.2.3+ by running > -> `pip install skin-tone-classifier --upgrade` +> `pip install skin-tone-classifier[all] --upgrade` +> +> 2. If you encounter the following problem: +> > This program needs access to the screen. Please run with a Framework +> > build of python, and only when you are logged in on the main display +> > of your Mac. > +> Please launch the GUI by running `pythonw -m stone` in the terminal. +> References: +> * [stackoverflow](https://stackoverflow.com/a/52732858/8860079) +> * [python-using-mac](https://docs.python.org/3/using/mac.html) ### Use `stone` in command line interface (CLI) @@ -569,172 +591,6 @@ The `result_json` will be like: } ``` -# Changelogs - -## v1.2.0 - -
- Click here to show more. - -In this version, we have made the following changes: - -1. ✨ **NEW!**: We add a GUI version of `stone` for users who are not familiar with the command line interface. - * You can use the config GUI of `stone` to process the images. - * See more information at [here](#use-stone-in-a-gui). - -
- -## v1.1.3 - -
- Click here to show more. - -In this version, we have made the following changes: - -1. ✨ **NEW!**: We add new **patterns** in the `-l` (or `--labels`) option to set the skin tone labels. - * Now, you can use the following patterns to set the skin tone labels: - * **Default value**: the uppercase alphabet list leading by the image type (`C` for `color`; `B` - for `Black&White`). - * Specify the labels directly using _a space_ as delimiters, e.g., `-l A B C D E` or `-l 1 2 3 4 5`. - * Specify the range of labels using _a hyphen_ as delimiters, e.g., - * `-l A-E` (equivalent to `-l A B C D E`); - * `-l A-E-2` (equivalent to `-l A C E`); - * `-l 1-5` (equivalent to `-l 1 2 3 4 5`); - * `-l 1-10-3` (equivalent to `-l 1 4 7 10`); - * **NB**: The number of skin tone labels should be equal to the number of colors in the palette. - -
- -## v1.1.2 - -
- Click here to show more. - -In this version, we have made the following changes: - -1. 🐛 **FIX!**: We fixed a bug where the app will crash when using the `-bw` option. - Error message: `cannot reshape array of size 62500 into shape (3)`. -2. 🐛 **FIX!**: We fixed a bug where the app may identify the image type as `color` when using the `-bw` option. - -
- -## v1.1.1 - -
- Click here to show more. - -In this version, we have made the following changes: - -1. ✨ **NEW!**: We add the `-v` (or `--version`) option to show the version number. -2. ✨ **NEW!**: We add the `-r` (or `--recursive`) option to **enable** recursive search for images. - * For example, `stone -i ./path/to/images/ -r` will search all images in the `./path/to/images/` directory **and its - subdirectories**. - * `stone -i ./path/to/images/` will only search images in the `./path/to/images/` directory. -3. 🐛 **FIX!**: We fixed a bug where the app cannot correctly identify the current folder if `-i` option is not - specified. - -
- -## v1.1.0 - -
- Click here to show more. - -In this version, we have made the following changes: - -1. ✨ **NEW!**: Now, `stone` can not only be run on **the command line**, but can also be **imported** into other - projects for use. Check [this](#9-used-as-a-library-by-importing-into-other-projects) for more details. - * We expose the `process` and `show` functions in the `stone` package. -2. ✨ **NEW!**: We add `URL` support for the input images. - * Now, you can specify the input image as a URL, e.g., `https://example.com/images/pic.jpg`. Of course, you can mix - the URLs and local filenames. -3. ✨ **NEW!**: We add **recursive search** support for the input images. - * Now, when you specify the input image as a directory, e.g., `./path/to/images/`. - The app will search all images in the directory recursively. -4. 🧬 **CHANGE!**: We change the column header in `result.csv`: - * `prop` => `percent` - * `PERLA` => `tone label` -5. 🐛 **FIX!**: We fixed a bug where the app would not correctly sort files that did not contain numbers in their - filenames. - -
- -## v1.0.1 - -
- Click here to show more. - -1. 👋 **BYE**: We have removed the function to pop up a resulting window when processing a **single** image. - - * It can raise an error when running the app in a **web browser** environment, e.g., Jupyter Notebook or Google - Colab. - * If you want to see the processed image, please use the `-d` option to store the report image in the `./debug` - folder. - -
- -## v1.0.0 - -
- Click here to show more. - -🎉**We have officially released the 1.0.0 version of the library!** In this version, we have made the following changes: - -1. ✨ **NEW!**: We add the `threshold` parameter to control the minimum percentage of required face areas (Defaults to - 0.15). - * In previous versions, the library could incorrectly identify non-face areas as faces, such as shirts, collars, - necks, etc. - In order to improve its accuracy, the new version will further calculate the proportion of skin in the recognized - area - after recognizing the facial area. If it is less than the `threshold` value, the recognition area will be ignored. - (While it's still not perfect, it's an improvement over what it was before.) -2. ✨ **NEW!**: Now, we will back up the previous results if it already exists. - The backup file will be named as `result_bak_.csv`. -3. 🐛 **FIX!**: We fix the bug that the `image_type` option does not work in the previous version. -4. 🐛 **FIX!**: We fix the bug that the library will create an empty `log` folder when checking the help information by - running `stone -h`. - -
- -## v0.2.0 - -
- Click here to show more. - -In this version, we have made the following changes: - -1. ✨ **NEW!**: Now we support skin tone classification for **black and white** images. - * In this case, the app will use different skin tone palettes for color images and black/white images. - * We use a new parameter `-t` or `--image_type` to specify the type of the input image. - It can be `color`, `bw` or `auto`(default). - `auto` will let the app automatically detect whether the input is color or black/white image. - * We use a new parameter `-bw` or `--black_white` to specify whether to convert the input to black/white image. - If so, the app will convert the input to black/white image and then classify the skin tones based on the - black/white palette. - - For example: -
- Processing color image - Processing black/white image -
- -2. ✨ **NEW!**: Now we support **multiprocessing** for processing the images. It will largely speed up the processing. - * The number of processes is set to the number of CPU cores by default. - * You can specify the number of processes by `--n_workers` parameter. -3. 🧬 **CHANGE!**: We add more details in the report image to facilitate the debugging, as shown above. - * We add the face id in the report image. - * We add the effective face or skin area in the report image. In this case, the other areas are blurred. -4. 🧬 **CHANGE!**: Now, we save the report images into different folders based on their `image_type` (color or - black/white) and the number of detected faces. - * For example, if the input image is **color** and there are **2 faces** detected, the report image will be saved - in `./debug/color/faces_2/` folder. - * If the input image is **black/white** and no face has been detected, the report image will be saved - in `./debug/bw/faces_0/` folder. - * You can easily to tune the parameters and rerun the app based on the report images in the corresponding folder. -5. 🐛 **FIX!**: We fix the bug that the app will crash when the input image has dimensionality errors. - * Now, the app won't crash and will report the error message in `./result.csv`. - -
# Citation @@ -782,9 +638,8 @@ If you are interested in our work, please cite: # Disclaimer -The images used in this project are from [Flickr-Faces-HQ Dataset (FFHQ)](https://github.com/NVlabs/ffhq-dataset), which -is licensed under -the [Creative Commons BY-NC-SA 4.0 license](https://github.com/NVlabs/ffhq-dataset/blob/master/LICENSE.txt) +The images used in this project are from [Flickr-Faces-HQ Dataset (FFHQ)](https://github.com/NVlabs/ffhq-dataset), +which is licensed under the [Creative Commons BY-NC-SA 4.0 license](https://github.com/NVlabs/ffhq-dataset/blob/master/LICENSE.txt). -Thank you for considering contributing to **SkinToneClassifier**. We value your input and look forward to collaborating -with you! +Thank you for considering contributing to **SkinToneClassifier**. +We value your input and look forward to collaborating with you! diff --git a/docs/illustration.svg b/docs/illustration.svg new file mode 100644 index 0000000..841b5ac --- /dev/null +++ b/docs/illustration.svg @@ -0,0 +1 @@ +Step 1. Face detectionOriginal portraitStep 2. Skin segmentation𝑑2=#𝐴66953𝜌2=33.68%𝑑1=#𝐸9𝐵𝐹𝐴3𝜌1=66.32%Step 3. Skin tone distillation ABCDEFGJKIHStep 4. Skin tone classification \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..427ecf3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,179 @@ +[project] +# This is the name of your project. The first time you publish this +# package, this name will be registered for you. It will determine how +# users can install this project, e.g.: +# +# $ pip install sampleproject +# +# And where it will live on PyPI: https://pypi.org/project/sampleproject/ +# +# There are some restrictions on what makes a valid project name +# specification here: +# https://packaging.python.org/specifications/core-metadata/#name +name = "skin-tone-classifier" # Required + +dynamic = ["version"] +# https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html#dynamic-metadata +# "dependencies", "optional-dependencies" are BETA features currently +#dynamic = ["version", "dependencies", "optional-dependencies"] + +# Versions should comply with PEP 440: +# https://www.python.org/dev/peps/pep-0440/ +# +# For a discussion on single-sourcing the version, see +# https://packaging.python.org/guides/single-sourcing-package-version/ +#https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers +#version = "1.2.3" # Required + +# This is a one-line description or tagline of what your project does. This +# corresponds to the "Summary" metadata field: +# https://packaging.python.org/specifications/core-metadata/#summary +description = "An easy-to-use library for skin tone classification" # Optional + +# This is an optional longer description of your project that represents +# the body of text which users will see when they visit PyPI. +# +# Often, this is the same as your README, so you can just read it in from +# that file directly (as we have already done above) +# +# This field corresponds to the "Description" metadata field: +# https://packaging.python.org/specifications/core-metadata/#description-optional +readme = "README.md" # Optional + +# Specify which Python versions you support. In contrast to the +# 'Programming Language' classifiers above, 'pip install' will check this +# and refuse to install the project if the version does not match. See +# https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires +requires-python = ">=3.9" + +# This is either text indicating the license for the distribution, or a file +# that contains the license +# https://packaging.python.org/en/latest/specifications/core-metadata/#license +#license = { file = "LICENSE" } + +# This field adds keywords for your project which will appear on the +# project page. What does your project relate to? +# +# Note that this is a list of additional keywords, separated +# by commas, to be used to assist searching for the distribution in a +# larger catalog. +keywords = ["skin tone", "image recognition", "face detection", "skin detection", "image segmentation"] # Optional + +# This should be your name or the name of the organization who originally +# authored the project, and a valid email address corresponding to the name +# listed. +authors = [ + { name = "Chenglong Ma", email = "chenglong.m@outlook.com" } # Optional +] + +# This should be your name or the names of the organization who currently +# maintains the project, and a valid email address corresponding to the name +# listed. +maintainers = [ + { name = "Chenglong Ma", email = "chenglong.m@outlook.com" } # Optional +] + +# Classifiers help users find your project by categorizing it. +# +# For a list of valid classifiers, see https://pypi.org/classifiers/ +classifiers = [# Optional + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering :: Image Recognition", + "Topic :: Scientific/Engineering :: Image Processing", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Sociology", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Terminals", + "Environment :: Console", + "Environment :: Web Environment", + "Environment :: Win32 (MS Windows)", + "Environment :: MacOS X", + "Environment :: Other Environment", +] + +# This field lists other packages that your project depends on to run. +# Any package you put here will be installed by pip when your project is +# installed, so they must be valid existing projects. +# +# For an analysis of this field vs pip's requirements files see: +# https://packaging.python.org/discussions/install-requires-vs-requirements/ +dependencies = [# Optional + "opencv-python>=4.9.0.80", + "numpy>=1.21.5", + "colormath>=3.0.0", + "tqdm>=4.64.0", + "colorama>=0.4.6", + "packaging>=23.1", + "requests>=2.31.0", +] + +# List additional groups of dependencies here (e.g. development +# dependencies). Users will be able to install these using the "extras" +# syntax, for example: +# +# $ pip install sampleproject[dev] +# +# Similar to `dependencies` above, these must be valid existing +# projects. +[project.optional-dependencies] # Optional +all = [ + "gooey>=1.0.8.1", + "re-wx==0.0.10", + "colored==1.3.93", +] + +# List URLs that are relevant to your project +# +# This field corresponds to the "Project-URL" and "Home-Page" metadata fields: +# https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use +# https://packaging.python.org/specifications/core-metadata/#home-page-optional +# +# Examples listed include a pattern for specifying where the package tracks +# issues, where the source is hosted, where to say thanks to the package +# maintainers, and where to support the project financially. The key is +# what's used to render the link text on PyPI. +[project.urls] # Optional +"Homepage" = "https://chenglongma.com/SkinToneClassifier/" +"Bug Reports" = "https://github.com/ChenglongMa/SkinToneClassifier/issues" +"Funding" = "https://github.com/sponsors/ChenglongMa" +"Say Thanks!" = "https://saythanks.io/to/ChenglongMa" +"Repository" = "https://github.com/ChenglongMa/SkinToneClassifier/" +Changelog = "https://github.com/ChenglongMa/SkinToneClassifier/blob/main/CHANGELOD.md" + +# The following would provide a command line executable called `sample` +# which executes the function `main` from this package when invoked. +[project.scripts] # Optional +stone = "stone.__main__:main" + +[project.gui-scripts] +stone-gui = "stone.__main__:main" + +# This is configuration specific to the `setuptools` build backend. +# If you are using a different build backend, you will need to change this. +[tool.setuptools] +# If there are data files included in your packages that need to be +# installed, specify them here. +package-dir = { "" = "src" } +license-files = ["LICENSE"] + +[tool.setuptools.dynamic] +version = { attr = "stone.package.__version__" } + +[build-system] +# These are the assumed default build requirements from pip: +# https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support +requires = ["setuptools>=66.1.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index e1afbce..f715ad8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ setuptools>=65.6.3 colorama>=0.4.6 packaging>=23.1 requests>=2.31.0 +# For GUI gooey>=1.0.8.1 colored==1.3.93 re-wx==0.0.10 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8af290b..0000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[metadata] -license_files = LICENSE -description_file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 4bdacab..0000000 --- a/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -from setuptools import setup -from setuptools import find_packages - -PACKAGE = {} -with open("src/stone/package.py", "r", encoding="utf-8") as fp: - exec(fp.read(), PACKAGE) - -with open("README.md", "r", encoding="utf-8") as f: - LONG_DESCRIPTION = f.read() - -setup( - name=PACKAGE["__package_name__"], - version=PACKAGE["__version__"], - description=PACKAGE["__description__"], - long_description=LONG_DESCRIPTION, - long_description_content_type="text/markdown", - packages=find_packages("src"), - package_dir={"": "src"}, - include_package_data=True, - zip_safe=False, - author=PACKAGE["__author__"], - author_email=PACKAGE["__author_email__"], - keywords="skin-tone image-recognition face-detection", - url=PACKAGE["__url__"], - project_urls={ - "Documentation": PACKAGE["__code__"], - "Code": PACKAGE["__code__"], - "Issue tracker": PACKAGE["__issues__"], - }, - entry_points={ - "console_scripts": ["stone = stone.__main__:main"], - }, - python_requires=">=3.9", - install_requires=[ - "opencv-python>=4.9.0.80", - "numpy>=1.21.5", - "colormath>=3.0.0", - "tqdm>=4.64.0", - "colorama>=0.4.6", - "packaging>=23.1", - "requests>=2.31.0", - "gooey>=1.0.8.1", - "re-wx==0.0.10", - "colored==1.3.93", - ], - classifiers=[ - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: OS Independent", - "Topic :: Scientific/Engineering :: Image Recognition", - "Topic :: Scientific/Engineering :: Image Processing", - "Environment :: Console", - "Environment :: Web Environment", - "Environment :: Win32 (MS Windows)", - "Environment :: MacOS X", - "Environment :: Other Environment", - ], -) diff --git a/src/stone/__main__.py b/src/stone/__main__.py index 0a9fd7b..d7720de 100644 --- a/src/stone/__main__.py +++ b/src/stone/__main__.py @@ -226,7 +226,11 @@ def write_to_csv(row: list): sys.argv.remove("--gui") if "--gui" in sys.argv else None if not use_cli and "--ignore-gooey" not in sys.argv: - from gooey import Gooey + try: + from gooey import Gooey + except ImportError: + # If gooey is not installed, use a dummy decorator + from stone.utils import Gooey from importlib.resources import files diff --git a/src/stone/package.py b/src/stone/package.py index 78ab4ae..2df6915 100644 --- a/src/stone/package.py +++ b/src/stone/package.py @@ -1,4 +1,4 @@ -__version__ = "1.2.2" +__version__ = "1.2.3" __package_name__ = "skin-tone-classifier" __app_name__ = "Skin Tone Classifier" __description__ = "An easy-to-use library for skin tone classification" diff --git a/src/stone/utils.py b/src/stone/utils.py index 478f7d7..cc18635 100644 --- a/src/stone/utils.py +++ b/src/stone/utils.py @@ -22,8 +22,22 @@ class ArgumentError(ValueError): pass -# @functools.cache # Python 3.9+ -@functools.lru_cache(maxsize=128) # Python 3.2+ +def Gooey(*args, **kwargs): + """ + Dummy decorator for Gooey. + Used in CLI mode to avoid the import error when the Gooey package is not installed. + :param args: + :param kwargs: + :return: + """ + + def inner(func): + return func + + return inner + + +@functools.cache def alphabet_id(n): letters = string.ascii_uppercase n_letters = len(letters) @@ -104,161 +118,36 @@ def is_debugging(): return gettrace is not None and gettrace() -def build_arguments_deprecated(): - print("Using CLI mode.") - LOG.info("LOG: Using CLI mode.") - parser = argparse.ArgumentParser( - description=f"{__app_name__} v{__version__}", - formatter_class=argparse.RawTextHelpFormatter, - ) - parser.add_argument( - "-i", - "--images", - nargs="+", - default=["./"], - metavar="IMAGE FILENAME", - help="Image filename(s) or URLs to process;\n" - 'Supports multiple values separated by space, e.g., "a.jpg b.png";\n' - 'Supports directory or file name(s), e.g., "./path/to/images/ a.jpg";\n' - 'Supports URL(s), e.g., "https://example.com/images/pic.jpg" since v1.1.0+.\n' - "The app will search all images in current directory in default.", - ) - parser.add_argument( - "-r", - "--recursive", - action="store_true", - help="Whether to search images recursively in the specified directory.", - ) - - parser.add_argument( - "-t", - "--image_type", - default="auto", - metavar="IMAGE TYPE", - help="Specify whether the input image(s) is/are colored or black/white.\n" - 'Valid choices are: "auto", "color" or "bw",\n' - 'Defaults to "auto", which will be detected automatically.', - choices=["auto", "color", "bw"], - ) - parser.add_argument( - "-p", - "--palette", - nargs="+", - metavar="PALETTE", - help="Skin tone palette;\n" - 'Supports RGB hex value leading by "#" or RGB values separated by comma(,),\n' - 'E.g., "-p #373028 #422811" or "-p 255,255,255 100,100,100"', - ) - parser.add_argument( - "-l", - "--labels", - nargs="+", - metavar="LABELS", - help="Skin tone labels;\n" - "Default values are the uppercase alphabet list leading by the image type ('C' for 'color'; 'B' for 'Black&White'), " - "e.g., ['CA', 'CB', ..., 'CZ'] or ['BA', 'BB', ..., 'BZ'].\n" - "Since v1.2.0, supports range of labels, e.g., 'A-Z' or '1-10'.\n" - "Refer to https://github.com/ChenglongMa/SkinToneClassifier for more details.", - ) - parser.add_argument( - "-d", - "--debug", - action="store_true", - help="Whether to generate report images, used for debugging and verification." - "The report images will be saved in the './debug' directory.", - ) - parser.add_argument( - "-bw", - "--black_white", - action="store_true", - help="Whether to convert the input to black/white image(s).\n" - "If true, the app will use the black/white palette to classify the image.", - ) - parser.add_argument( - "-o", - "--output", - default="./", - metavar="DIRECTORY", - help="The path of output file, defaults to current directory.", - ) - parser.add_argument( - "--n_workers", - type=int, - metavar="WORKERS", - help="The number of workers to process the images, defaults to the number of CPUs in the system.", - default=0, - ) - - parser.add_argument( - "--n_colors", - type=int, - metavar="COLORS", - help="CONFIG: the number of dominant colors to be extracted, defaults to 2.", - default=2, - ) - parser.add_argument( - "--new_width", - type=int, - metavar="WIDTH", - help="CONFIG: resize the images with the specified width. Negative value will be ignored, defaults to 250.", - default=250, - ) - - # For the next parameters, refer to https://stackoverflow.com/a/20805153/8860079 - parser.add_argument( - "--scale", - type=float, - metavar="SCALE", - help="CONFIG: how much the image size is reduced at each image scale, defaults to 1.1", - default=1.1, - ) - parser.add_argument( - "--min_nbrs", - type=int, - metavar="NEIGHBORS", - help="CONFIG: how many neighbors each candidate rectangle should have to retain it.\n" - "Higher value results in less detections but with higher quality, defaults to 5.", - default=5, - ) - parser.add_argument( - "--min_size", - type=int, - nargs="+", - metavar=("WIDTH", "HEIGHT"), - help='CONFIG: minimum possible face size. Faces smaller than that are ignored, defaults to "90 90".', - default=(90, 90), - ) - parser.add_argument( - "--threshold", - type=float, - metavar="THRESHOLD", - help="CONFIG: what percentage of the skin area is required to identify the face, defaults to 0.15.", - default=0.15, - ) - parser.add_argument( - "-v", - "--version", - action="version", - version=f"%(prog)s {__version__}", - help="Show the version number and exit.", - ) +def build_arguments(): + try: + from gooey import GooeyParser - return parser.parse_args() + in_gui = True + except ImportError: + from argparse import ArgumentParser as GooeyParser + in_gui = False -def build_arguments(): - from gooey import GooeyParser - + kwargs = dict(formatter_class=argparse.RawTextHelpFormatter) if not in_gui else {} parser = GooeyParser( description=__description__, + **kwargs, + ) + kwargs = ( + { + "gooey_options": {"show_border": False, "columns": 1}, + } + if in_gui + else {} ) files = parser.add_argument_group( "Images to process", "The locations of images to process, which can be directories, files, or URLs.\n" "Multiple values are separated by space;\n" 'You can mix folders, filenames and web links together, e.g., "/path/to/dir1 /path/to/pic.jpg https://example.com/pic.png".\n', - gooey_options={"show_border": False, "columns": 1}, + **kwargs, ) + kwargs = {"gooey_options": {"visible": False}} if in_gui else {} files.add_argument( "-i", @@ -266,80 +155,82 @@ def build_arguments(): nargs="+", default=[os.getcwd()], metavar="Image Filenames", - help="Image filename(s), Directories or URLs to process.", - gooey_options={"visible": False}, - ) - files.add_argument( - "--image_dirs", - nargs="+", - metavar="Image Directories", - widget="DirChooser", - # widget="MultiDirChooser", # fixme: enable this widget when issues are fixed - gooey_options={ - "message": "Select directories to process", - "initial_value": os.getcwd(), - "default_path": os.getcwd(), - "placeholder": "e.g., /path/to/dir1 /path/to/dir2", - }, - ) + help="Image filename(s), Directories or URLs to process. Separated by space.", + **kwargs, + ) + if in_gui: + files.add_argument( + "--image_dirs", + nargs="+", + metavar="Image Directories", + widget="DirChooser", + # widget="MultiDirChooser", # fixme: enable this widget when issues are fixed + gooey_options={ + "message": "Select directories to process", + "initial_value": os.getcwd(), + "default_path": os.getcwd(), + "placeholder": "e.g., /path/to/dir1 /path/to/dir2", + }, + ) + kwargs = dict(metavar="Recursive Search") if in_gui else {} files.add_argument( "-r", "--recursive", - metavar="Recursive Search", action="store_true", help="Search images recursively in the specified directory.", - ) - - files.add_argument( - "--image_files", - nargs="+", - metavar="Image Filenames", - help="Add individual image file(s)", - widget="MultiFileChooser", - gooey_options={ - "wildcard": "All images|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tif;*.webp|" - "JPG (*.jpg)|*.jpg|" - "JPEG (*.jpeg)|*.jpeg|" - "PNG (*.png)|*.png|" - "BMP (*.bmp)|*.bmp|" - "GIF (*.gif)|*.gif|" - "TIFF (*.tif)|*.tif|" - "WEBP (*.webp)|*.webp|" - "All files (*.*)|*.*", - "message": "Select the image file(s) to process", - "default_dir": os.getcwd(), - "full_width": False, - "placeholder": "e.g., a.jpg b.png", - }, - ) + **kwargs, + ) + if in_gui: + files.add_argument( + "--image_files", + nargs="+", + metavar="Image Filenames", + help="Add individual image file(s)", + widget="MultiFileChooser", + gooey_options={ + "wildcard": "All images|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tif;*.webp|" + "JPG (*.jpg)|*.jpg|" + "JPEG (*.jpeg)|*.jpeg|" + "PNG (*.png)|*.png|" + "BMP (*.bmp)|*.bmp|" + "GIF (*.gif)|*.gif|" + "TIFF (*.tif)|*.tif|" + "WEBP (*.webp)|*.webp|" + "All files (*.*)|*.*", + "message": "Select the image file(s) to process", + "default_dir": os.getcwd(), + "full_width": False, + "placeholder": "e.g., a.jpg b.png", + }, + ) - files.add_argument( - "--image_urls", - nargs="+", - metavar="Image URLs", - help="Add image URLs", - gooey_options={ - "full_width": False, - "placeholder": "e.g., https://example.com/a.jpg https://example.com/b.png", - }, - ) + files.add_argument( + "--image_urls", + nargs="+", + metavar="Image URLs", + help="Add image URLs", + gooey_options={ + "full_width": False, + "placeholder": "e.g., https://example.com/a.jpg https://example.com/b.png", + }, + ) + kwargs = {"gooey_options": {"show_border": False, "columns": 2}} if in_gui else {} images = parser.add_argument_group( "Image Settings", - gooey_options={ - "show_border": False, - "columns": 2, - }, + **kwargs, ) + bw_option = "black/white" if in_gui else "bw" images.add_argument( "-t", "--image_type", default="auto", metavar="Image Type", help="Specify whether the input image(s) is/are colored or black/white.\n" - 'Defaults to "auto", which will be detected automatically.', - choices=["auto", "color", "bw"], + f'Defaults to "auto", which will be detected automatically. Other options are "color" and "{bw_option}".\n', + choices=["auto", "color", bw_option], ) + kwargs = {"gooey_options": {"full_width": True}} if in_gui else {} images.add_argument( "-p", "--palette", @@ -349,7 +240,7 @@ def build_arguments(): 'Input RGB hex values leading by "#" or RGB values separated by comma(,),\n' "E.g., #373028 #422811 or 255,255,255 100,100,100\n" "Leave blank to use the default palette as mentioned in the document.\n", - gooey_options={"full_width": True}, + **kwargs, ) images.add_argument( "-l", @@ -361,18 +252,24 @@ def build_arguments(): "e.g., ['CA', 'CB', ..., 'CZ'] or ['BA', 'BB', ..., 'BZ'].\n" "Since v1.2.0, supports range of labels, e.g., 'A-Z' or '1-10'.\n" "Refer to https://github.com/ChenglongMa/SkinToneClassifier#3-specify-category-labels for more details.", - gooey_options={"full_width": True}, + **kwargs, ) + kwargs = dict(metavar="Convert to Black/White") if in_gui else {} images.add_argument( "-bw", "--black_white", - metavar="Convert to Black/White", action="store_true", help="Whether to convert the input to black/white image(s)?\n" - "If true, the app will use the black/white palette to classify the image.", + "If true, the app will convert the input to black/white image(s) and use the black/white palette for classification.", + **kwargs, ) + kwargs = ( + {"gooey_options": {"initial_value": 2, "min": 1, "max": 99999, "full_width": False}, "widget": "IntegerField"} + if in_gui + else {} + ) images.add_argument( "--n_colors", metavar="Number of Dominant Colors", @@ -380,8 +277,16 @@ def build_arguments(): help="Specify the number of dominant colors to be extracted.\n" "The colors will be used to compare with the colors in the palette.\n", default=2, - widget="IntegerField", - gooey_options={"initial_value": 2, "min": 1, "max": 99999, "full_width": False}, + **kwargs, + ) + + kwargs = ( + { + "gooey_options": {"initial_value": 250, "min": 10, "max": 99999, "full_width": False}, + "widget": "IntegerField", + } + if in_gui + else {} ) images.add_argument( "--new_width", @@ -391,37 +296,49 @@ def build_arguments(): "Sometimes smaller images will be processed faster and more accurately.\n" "No resizing will be performed if the value is negative.", default=250, - widget="IntegerField", - gooey_options={"initial_value": 250, "min": 10, "max": 99999, "full_width": False}, + **kwargs, ) - outputs = parser.add_argument_group("Output Settings", gooey_options={"show_border": True}) + kwargs = {"gooey_options": {"show_border": True}} if in_gui else {} + outputs = parser.add_argument_group("Output Settings", **kwargs) + + kwargs = ( + { + "gooey_options": {"message": "Select the output directory", "default_path": os.getcwd()}, + "widget": "DirChooser", + } + if in_gui + else {} + ) outputs.add_argument( "-o", "--output", metavar="Output Directory", default=os.getcwd(), help="Specify the path of output file, defaults to current directory.", - widget="DirChooser", - gooey_options={ - "message": "Select the output directory", - "default_path": os.getcwd(), - }, + **kwargs, ) + + kwargs = dict(metavar="Generate Report Images") if in_gui else {} outputs.add_argument( "-d", "--debug", - metavar="Generate Report Images", action="store_true", - default=True, + default=in_gui, help="Whether to generate report images?\n" "If true, the report images will be saved in the '/debug' directory.", + **kwargs, ) + kwargs = {"gooey_options": {"show_border": False, "columns": 2}} if in_gui else {} advanced = parser.add_argument_group( "Advanced Settings", "For advanced users only, please refer to https://stackoverflow.com/a/20805153/8860079", - gooey_options={"show_border": False, "columns": 2}, + **kwargs, + ) + + kwargs = ( + {"gooey_options": {"initial_value": 1.1, "min": 0.1, "max": 2.0}, "widget": "DecimalField"} if in_gui else {} ) advanced.add_argument( "--scale", @@ -429,9 +346,10 @@ def build_arguments(): metavar="Scale", help="Specify how much the image size is reduced at each image scale.", default=1.1, - widget="DecimalField", - gooey_options={"initial_value": 1.1, "min": 0.1, "max": 2.0}, + **kwargs, ) + + kwargs = {"gooey_options": {"initial_value": 5, "min": 1, "max": 99999}, "widget": "IntegerField"} if in_gui else {} advanced.add_argument( "--min_nbrs", type=int, @@ -439,59 +357,61 @@ def build_arguments(): help="Specify how many neighbors each candidate rectangle should have to retain it.\n" "Higher value results in less detections but with higher quality.", default=5, - widget="IntegerField", - gooey_options={"initial_value": 5, "min": 1, "max": 99999}, + **kwargs, ) default_min_width = 90 default_min_height = 90 + kwargs = {"gooey_options": {"visible": False}} if in_gui else {} advanced.add_argument( "--min_size", type=int, nargs="+", metavar="Minimum Possible Face Size, format: ", - help=f'[Alias --min_width, --min_height] Specify the minimum possible face size. Faces smaller than that are ignored, defaults to "{default_min_width} {default_min_height}".', + help=f'Specify the minimum possible face size. Faces smaller than that are ignored, defaults to "{default_min_width} {default_min_height}".', default=(default_min_width, default_min_height), - gooey_options={ - "visible": False, - }, + **kwargs, ) + if in_gui: + min_size = advanced.add_argument_group( + "Minimum Possible Face Size (pixels)", + 'Specify the minimum possible face size. Faces smaller than that are ignored, defaults to "90 90".', + gooey_options={"show_border": True, "columns": 2}, + ) - min_size = advanced.add_argument_group( - "Minimum Possible Face Size (pixels)", - 'Specify the minimum possible face size. Faces smaller than that are ignored, defaults to "90 90".', - gooey_options={"show_border": True, "columns": 2}, - ) + min_size.add_argument( + "--min_width", + type=int, + metavar="Minimum Width", + # help="Specify the minimum possible face width. Faces smaller than that are ignored, defaults to 90.", + default=default_min_width, + widget="IntegerField", + gooey_options={"initial_value": default_min_width, "min": 10, "max": 99999}, + ) - min_size.add_argument( - "--min_width", - type=int, - metavar="Minimum Width", - # help="Specify the minimum possible face width. Faces smaller than that are ignored, defaults to 90.", - default=default_min_width, - widget="IntegerField", - gooey_options={"initial_value": default_min_width, "min": 10, "max": 99999}, - ) + min_size.add_argument( + "--min_height", + type=int, + metavar="Minimum Height", + # help="Specify the minimum possible face height. Faces smaller than that are ignored, defaults to 90.", + default=default_min_height, + widget="IntegerField", + gooey_options={"initial_value": default_min_height, "min": 10, "max": 99999}, + ) - min_size.add_argument( - "--min_height", - type=int, - metavar="Minimum Height", - # help="Specify the minimum possible face height. Faces smaller than that are ignored, defaults to 90.", - default=default_min_height, - widget="IntegerField", - gooey_options={"initial_value": default_min_height, "min": 10, "max": 99999}, + kwargs = ( + {"gooey_options": {"initial_value": 0.15, "min": 0.01, "max": 1.0}, "widget": "DecimalField"} if in_gui else {} ) - advanced.add_argument( "--threshold", type=float, metavar="Minimum Possible Face Proportion", help="Specify the minimum proportion of the skin area required to identify the face, defaults to 0.15.", default=0.15, - widget="DecimalField", - gooey_options={"initial_value": 0.15, "min": 0.01, "max": 1.0}, + **kwargs, ) + + kwargs = {"gooey_options": {"initial_value": 0, "min": 0, "max": 99999}, "widget": "IntegerField"} if in_gui else {} advanced.add_argument( "--n_workers", type=int, @@ -499,28 +419,32 @@ def build_arguments(): help="Specify the number of workers to process the images.\n" "0 means the total number of CPU cores in the system.", default=0, - widget="IntegerField", - gooey_options={"initial_value": 0, "min": 0, "max": 99999}, + **kwargs, ) + kwargs = dict(gooey_options={"visible": False}) if in_gui else {} advanced.add_argument( "-v", "--version", action="version", version=f"%(prog)s {__version__}", help="Show the version number and exit.", - gooey_options={"visible": False}, + **kwargs, ) args = parser.parse_args() images = args.images or [] - if args.image_dirs: + if getattr(args, "image_dirs", False): images.extend(args.image_dirs) - if args.image_files: + if getattr(args, "image_files", False): images.extend(args.image_files) - if args.image_urls: + if getattr(args, "image_urls", False): images.extend(args.image_urls) args.images = images - if tuple(args.min_size) == (default_min_width, default_min_height): + if ( + tuple(args.min_size) == (default_min_width, default_min_height) + and getattr(args, "min_width", False) + and getattr(args, "min_height", False) + ): args.min_size = (args.min_width, args.min_height) return args @@ -598,7 +522,7 @@ def check_version(): print( Fore.YELLOW + f"You are using an outdated version of {__package_name__} ({installed_version}).\n" f"Please upgrade to the latest version ({latest_version}) with the following command:\n", - Fore.GREEN + f"pip install {__package_name__} --upgrade\n" + Fore.RESET, + Fore.GREEN + f"pip install {__package_name__}[all] --upgrade\n" + Fore.RESET, ) os.environ["STONE_UPGRADE_FLAG"] = "1" except Exception: diff --git a/tests/test_utils.py b/tests/test_utils.py index 1c786d5..5c0ab55 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,13 +1,12 @@ import unittest from pathlib import Path -from unittest.mock import patch from stone.utils import build_image_paths, resolve_labels class TestUtils(unittest.TestCase): def setUp(self): - self.image_path = "./mock_data/images" + self.image_path = str(Path("./mock_data/images").resolve()) # Sorted image paths self.expected_recursive_image_paths = [ f"{self.image_path}/fake_img_1.gif", # In default, sorted by the trailing number @@ -60,7 +59,7 @@ def test_single_directory_non_recursive(self): self.should_exclude_folder(image_paths, ["subfolder", "debug", "log"]) self.assertListEqual( image_paths, - [Path(p) for p in self.expected_non_recursive_image_paths], + [Path(p).resolve() for p in self.expected_non_recursive_image_paths], ) def test_multiple_directories_recursive(self): diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d59d625 --- /dev/null +++ b/tox.ini @@ -0,0 +1,52 @@ +# this file is *not* meant to cover or endorse the use of tox or pytest or +# testing in general, +# +# It's meant to show the use of: +# +# - check-manifest +# confirm items checked into vcs are in your sdist +# - readme_renderer (when using a ReStructuredText README) +# confirms your long_description will render correctly on PyPI. +# +# and also to help confirm pull requests to this project. + +[tox] +envlist = py{39,310,311,312} + +# Define the minimal tox version required to run; +# if the host tox is less than this the tool with create an environment and +# provision it with a tox that satisfies it under provision_tox_env. +# At least this version is needed for PEP 517/518 support. +minversion = 3.3.0 + +# Activate isolated build environment. tox will use a virtual environment +# to build a source distribution from the source tree. For build tools and +# arguments use the pyproject.toml file as specified in PEP-517 and PEP-518. +isolated_build = true + +[testenv] +deps = + check-manifest >= 0.42 + # If your project uses README.rst, uncomment the following: + # readme_renderer + # flake8 + # pytest + build + twine + +allowlist_externals = + cp + +commands = + check-manifest --ignore 'tox.ini,tests/**' + python -m build + python -m twine check dist/* + # flake8 . + # python -m unittest discover -v + cp -r tests/mock_data ./ + python -m unittest discover -v tests + # py.test tests {posargs} + +# [flake8] +# exclude = .tox,*.egg,build,data +# select = E,W,F \ No newline at end of file