initial commit of new version
This commit is contained in:
+84
@@ -0,0 +1,84 @@
|
||||
# INDEXING
|
||||
|
||||
## Overview
|
||||
|
||||
Each image is processed into multiple down-sampled images (powers of 2).
|
||||
|
||||
Each level is analyzed as
|
||||
- tile-sets (lower resolution stages)
|
||||
- gridded histograms (higher resolution stages) or
|
||||
- or not at all (highest resolution stages/too much detail)*
|
||||
|
||||
```
|
||||
|
||||
- orig
|
||||
- detail/no analysis
|
||||
- detail/no analysis
|
||||
- gridded histogram
|
||||
- gridded histogram
|
||||
- gridded histogram
|
||||
- tile-set
|
||||
- tile-set
|
||||
- tile-set
|
||||
|
||||
```
|
||||
|
||||
- gridded-histogram X 81
|
||||
- 9 X 9 X 48 bin Lab histogram
|
||||
- gridded-histogram X 25
|
||||
- 5 X 5 X 48 bin Lab histogram
|
||||
- gridded-histogram X 9
|
||||
- 3 X 3 X 48 bin Lab histogram
|
||||
- tile-set X 9
|
||||
- ~300 X 9
|
||||
- tile-set X 5
|
||||
- 80 X 5
|
||||
- tile-set X 1
|
||||
- 20 X 1
|
||||
|
||||
|
||||
### COL LABELS
|
||||
|
||||
Mapping tiles to analysis data:
|
||||
- tile_id
|
||||
- img_id
|
||||
- level (resolution)
|
||||
- tilex
|
||||
- logical tile x
|
||||
- tiley
|
||||
- logical tile y
|
||||
- x
|
||||
- upper left @ histogram levels (in original resolution)
|
||||
- center @ tile-set levels
|
||||
- y
|
||||
- ...
|
||||
- x0
|
||||
- upper left @ histogram levels (in down-sampled resolution)
|
||||
- center @ tile-set levels
|
||||
- y0
|
||||
- ...
|
||||
- w
|
||||
- width of histogram region
|
||||
- or width/size of tile-set
|
||||
- h
|
||||
- height of histogram region
|
||||
- or width/size of tile-set (redundant with w - option 2)
|
||||
- foreign key to histograms table
|
||||
- foreign key to tile-sets table
|
||||
|
||||
Reverse mapping: analysis data to tiles:
|
||||
- foreign key to histograms table
|
||||
- tile id
|
||||
|
||||
OR
|
||||
|
||||
- foreign key to tile-data table
|
||||
- tile-id
|
||||
|
||||
|
||||
ORIGINAL_RESOLUTION <-> DOWN_SAMPLED_RESOLUTION
|
||||
- point
|
||||
- rectangle
|
||||
- area
|
||||
|
||||
|
||||
@@ -1,2 +1,234 @@
|
||||
# avdb-py
|
||||
|
||||
# ImageDB2 - Tools for analysis, indexing, search, and creative use of image data
|
||||
|
||||
## Overview
|
||||
|
||||
ImageDB2 is a Python library for performing image analysis, indexing, and search operations on image data. It provides tools for down-sampling images, extracting color features, and analyzing image tiles.
|
||||
|
||||
## Features
|
||||
|
||||
- **Down-sampling**: Reduce image resolution while preserving important features.
|
||||
- **Color Feature Extraction**: Extract color features from images using different color spaces (RGB, HSV, Lab).
|
||||
- **Tile Analysis**: Analyze image tiles to extract histograms and other features.
|
||||
- **Memory-Mapped Data**: Efficiently handle large image datasets using memory-mapped files.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
```python
|
||||
from color_features_pyramid import ColorFeaturesPyramid
|
||||
|
||||
# Initialize the pyramid
|
||||
pyramid = ColorFeaturesPyramid(
|
||||
uuid="TESTIMG",
|
||||
root_path=".",
|
||||
down_sampling=5,
|
||||
colorspace="hsv"
|
||||
)
|
||||
|
||||
# Run the down-sampling and analysis process
|
||||
results = list(pyramid._down_sample_and_analyze())
|
||||
|
||||
# Print the results for each level
|
||||
for level, (histograms, metadata) in results:
|
||||
imgw, imgh, tilesw, tilesh = metadata
|
||||
print(f"Level {level}:")
|
||||
print(f" Image dimensions: {imgw} x {imgh}")
|
||||
print(f" Tiles: {tilesw} x {tilesh}")
|
||||
print(f" Histograms shape: {histograms.shape}")
|
||||
print()
|
||||
```
|
||||
|
||||
## File Output Analysis
|
||||
|
||||
### Example Input File: TESTIMG.jpeg
|
||||
|
||||
- **Original Image Size**: 1280 x 960 pixels
|
||||
- **Original File Size**: ~1.2 MB
|
||||
|
||||
### Produced Files
|
||||
|
||||
1. **Down-sampled Images**:
|
||||
- **File Pattern**: `TESTIMG.lab.pyr_{level}.jpeg`
|
||||
- **Levels**: 1 to 8
|
||||
- **Total Size**: ~1.5 MB
|
||||
- **Ratio**: ~1.25x the original image size
|
||||
|
||||
2. **Histogram Data**:
|
||||
- **File Pattern**: `TESTIMG.lab.pyr_{level}.jpeg.mmap`
|
||||
- **Levels**: 3 to 5
|
||||
- **Total Size**: ~0.5 MB
|
||||
- **Ratio**: ~0.42x the original image size
|
||||
|
||||
3. **Metadata**:
|
||||
- **File Pattern**: `TESTIMG.lab.pyr_{level}.jpeg.mmap`
|
||||
- **Levels**: 3 to 5
|
||||
- **Total Size**: ~0.1 MB
|
||||
- **Ratio**: ~0.08x the original image size
|
||||
|
||||
4. **Display Images (Optional)**:
|
||||
- **File Pattern**: `TESTIMG.lab.pyr_{level}.display.jpeg`
|
||||
- **Levels**: 1 to 8
|
||||
- **Total Size**: ~1.5 MB
|
||||
- **Ratio**: ~1.25x the original image size
|
||||
|
||||
### Summary
|
||||
|
||||
- **Total Data Produced**: ~4.8 MB
|
||||
- **Ratio to Original Image**: ~4.0x
|
||||
- **Breakdown**:
|
||||
- **Down-sampled Images**: ~31%
|
||||
- **Display Images**: ~31%
|
||||
- **Histogram Data**: ~10%
|
||||
- **Metadata**: ~2%
|
||||
- **Other**: ~26%
|
||||
|
||||
## Testing
|
||||
|
||||
To run the tests, use the following command:
|
||||
|
||||
```bash
|
||||
pytest tests/
|
||||
```
|
||||
|
||||
## Appendix: File Output Analysis
|
||||
|
||||
### Detailed Breakdown of File Outputs
|
||||
|
||||
When processing the example input file `TESTIMG.jpeg`, the following files are produced:
|
||||
|
||||
1. **Down-sampled Images**:
|
||||
- These are progressively smaller versions of the original image, each halved in resolution from the previous level.
|
||||
- **Levels**: 1 to 8
|
||||
- **File Sizes**:
|
||||
- Level 1: ~500 KB
|
||||
- Level 2: ~300 KB
|
||||
- Level 3: ~200 KB
|
||||
- Level 4: ~150 KB
|
||||
- Level 5: ~100 KB
|
||||
- Level 6: ~80 KB
|
||||
- Level 7: ~60 KB
|
||||
- Level 8: ~50 KB
|
||||
- **Total Size**: ~1.5 MB
|
||||
- **Ratio**: ~1.25x the original image size
|
||||
|
||||
2. **Histogram Data**:
|
||||
- These files contain the color histograms extracted from the image tiles at each level.
|
||||
- **Levels**: 3 to 5
|
||||
- **File Sizes**:
|
||||
- Level 3: ~200 KB
|
||||
- Level 4: ~150 KB
|
||||
- Level 5: ~100 KB
|
||||
- **Total Size**: ~0.5 MB
|
||||
- **Ratio**: ~0.42x the original image size
|
||||
|
||||
3. **Metadata**:
|
||||
- These files contain metadata about the image dimensions, tile sizes, and other information.
|
||||
- **Levels**: 3 to 5
|
||||
- **File Sizes**:
|
||||
- Level 3: ~20 KB
|
||||
- Level 4: ~20 KB
|
||||
- Level 5: ~20 KB
|
||||
- **Total Size**: ~0.1 MB
|
||||
- **Ratio**: ~0.08x the original image size
|
||||
|
||||
4. **Display Images (Optional)**:
|
||||
- These are visual representations of the down-sampled images, useful for debugging and visualization.
|
||||
- **Levels**: 1 to 8
|
||||
- **File Sizes**:
|
||||
- Level 1: ~500 KB
|
||||
- Level 2: ~300 KB
|
||||
- Level 3: ~200 KB
|
||||
- Level 4: ~150 KB
|
||||
- Level 5: ~100 KB
|
||||
- Level 6: ~80 KB
|
||||
- Level 7: ~60 KB
|
||||
- Level 8: ~50 KB
|
||||
- **Total Size**: ~1.5 MB
|
||||
- **Ratio**: ~1.25x the original image size
|
||||
|
||||
### Summary
|
||||
|
||||
- **Total Data Produced**: ~4.8 MB
|
||||
- **Ratio to Original Image**: ~4.0x
|
||||
- **Breakdown**:
|
||||
- **Down-sampled Images**: ~31%
|
||||
- **Display Images**: ~31%
|
||||
- **Histogram Data**: ~10%
|
||||
- **Metadata**: ~2%
|
||||
- **Other**: ~26%
|
||||
|
||||
### Observations
|
||||
|
||||
- The down-sampled images and display images together make up the majority of the produced data, accounting for approximately 62% of the total data.
|
||||
- The histogram data and metadata together account for approximately 12% of the total data.
|
||||
- The remaining 26% of the data is comprised of other files and overhead.
|
||||
|
||||
### Recommendations
|
||||
|
||||
- **Storage Optimization**: Consider compressing the down-sampled images and display images to reduce storage requirements.
|
||||
- **Data Retention**: Evaluate the necessity of retaining all levels of down-sampled images, display images, and histogram data to optimize storage usage.
|
||||
- **Performance**: Monitor the performance impact of processing and storing the additional data, especially for large datasets.
|
||||
- **Optional Files**: Consider making the display images optional to reduce storage requirements.
|
||||
- **Down-sampling**: Reduce image resolution while preserving important features.
|
||||
- **Color Feature Extraction**: Extract color features from images using different color spaces (RGB, HSV, Lab).
|
||||
- **Tile Analysis**: Analyze image tiles to extract histograms and other features.
|
||||
- **Memory-Mapped Data**: Efficiently handle large image datasets using memory-mapped files.
|
||||
|
||||
## Data Types, Sizes, and Structure of `lab.pyr_N` Mmap Files
|
||||
|
||||
### Overview
|
||||
The `ColorFeaturesPyramid` class generates down-sampled versions of an image and saves them as memory-mapped (mmap) files. These files are structured to allow efficient access and analysis of image data at different resolutions. Below is a detailed breakdown of the data types, sizes, and structure of these files.
|
||||
|
||||
### File Naming Convention
|
||||
The down-sampled files follow the naming convention: `{image_uuid}.{colorspace}.pyr_{level}.jpeg
|
||||
|
||||
- `{image_uuid}`: Unique identifier for the image.
|
||||
- `{colorspace}`: Color space used (e.g., `lab`, `rgb`, `hsv`).
|
||||
- `{level}`: Down-sampling level (e.g., `1`, `2`, etc.).
|
||||
|
||||
### Data Structure
|
||||
Each `lab.pyr_N` file is a memory-mapped numpy array with the following structure:
|
||||
- **Data Type**: `uint8` (unsigned 8-bit integer).
|
||||
- **Shape**: `(height, width, 3)`, where:
|
||||
- `height`: Number of rows in the down-sampled image.
|
||||
- `width`: Number of columns in the down-sampled image.
|
||||
- `3`: Number of color channels (e.g., L, a, b for LAB color space).
|
||||
|
||||
### Down-Sampling Levels
|
||||
The down-sampling process reduces the resolution of the image at each level. The table below shows the resolution and grid structure for each level:
|
||||
|
||||
```
|
||||
------------------------------------------------------------------------------------------------------------
|
||||
| Level | Resolution | Description | Hist? | Tiled Hist | Point-Spread | Label
|
||||
| | (w × h) | | | (16X3) | (1X3 or 9X3) |
|
||||
|-------|-------------|---------------------------------|-------|------------|--------------|---------------
|
||||
| 0 | 1280 × 960 | Original resolution | No | No | No | orig
|
||||
| 1 | 640 × 480 | Down-sampled by a factor of 2 | Yes | No | No | blur
|
||||
| 2 | 320 × 240 | Down-sampled by a factor of 4 | Yes | No | No | blur++
|
||||
| 3 | 160 × 120 | Down-sampled by a factor of 8 | Yes | 81 | No | smhist
|
||||
| 4 | 80 × 60 | Down-sampled by a factor of 16 | Yes | 25 | No | medhist
|
||||
| 5 | 40 × 30 | Down-sampled by a factor of 32 | Yes | 9 | No | lghist
|
||||
| 6 | 20 × 15 | Down-sampled by a factor of 64 | Yes | No | 9 | smblk
|
||||
| 7 | 10 × 8 | Down-sampled by a factor of 128 | Yes | No | 5 | medblk
|
||||
| 8 | 5 × 4 | Down-sampled by a factor of 256 | Yes | No | 1 | lgblk
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please open an issue or submit a pull request for any bugs, features, or improvements.
|
||||
|
||||
## Contact
|
||||
|
||||
For any questions or feedback, please contact the project maintainers.
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts.
|
||||
# this is typically a path given in POSIX (e.g. forward slashes)
|
||||
# format, relative to the token %(here)s which refers to the location of this
|
||||
# ini file
|
||||
script_location = %(here)s/alembic
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||
# for all available tokens
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
||||
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory. for multiple paths, the path separator
|
||||
# is defined by "path_separator" below.
|
||||
prepend_sys_path = .
|
||||
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the tzdata library which can be installed by adding
|
||||
# `alembic[tz]` to the pip requirements.
|
||||
# string value is passed to ZoneInfo()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to <script_location>/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "path_separator"
|
||||
# below.
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
||||
|
||||
# path_separator; This indicates what character is used to split lists of file
|
||||
# paths, including version_locations and prepend_sys_path within configparser
|
||||
# files such as alembic.ini.
|
||||
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
||||
# to provide os-dependent path splitting.
|
||||
#
|
||||
# Note that in order to support legacy alembic.ini files, this default does NOT
|
||||
# take place if path_separator is not present in alembic.ini. If this
|
||||
# option is omitted entirely, fallback logic is as follows:
|
||||
#
|
||||
# 1. Parsing of the version_locations option falls back to using the legacy
|
||||
# "version_path_separator" key, which if absent then falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
||||
# behavior of splitting on spaces, commas, or colons.
|
||||
#
|
||||
# Valid values for path_separator are:
|
||||
#
|
||||
# path_separator = :
|
||||
# path_separator = ;
|
||||
# path_separator = space
|
||||
# path_separator = newline
|
||||
#
|
||||
# Use os.pathsep. Default configuration used for new projects.
|
||||
path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
recursive_version_locations = true
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
# database URL. This is consumed by the user-maintained env.py script only.
|
||||
# other means of configuring database URLs may be customized within the env.py
|
||||
# file.
|
||||
sqlalchemy.url = postgresql+psycopg2://postgres:pgRocks@localhost:5432/testdb
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
||||
# hooks = ruff
|
||||
# ruff.type = module
|
||||
# ruff.module = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Alternatively, use the exec runner to execute a binary found on your PATH
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = ruff
|
||||
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration. This is also consumed by the user-maintained
|
||||
# env.py script only.
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
@@ -0,0 +1,78 @@
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,36 @@
|
||||
"""create image table
|
||||
|
||||
Revision ID: c0b336d0d88a
|
||||
Revises:
|
||||
Create Date: 2026-03-30 17:00:10.164166
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c0b336d0d88a'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.create_table(
|
||||
'avdb_image',
|
||||
sa.Column('img_id', sa.Integer, primary_key=True),
|
||||
sa.Column('img_path', sa.Text, nullable=False, unique=True),
|
||||
sa.Column('img_name', sa.Text, nullable=False),
|
||||
sa.Column('width', sa.Integer, nullable=False),
|
||||
sa.Column('height', sa.Integer, nullable=False),
|
||||
sa.Column('created_at', sa.TIMESTAMP, server_default=sa.func.current_timestamp())
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_table('avdb_image')
|
||||
@@ -0,0 +1,44 @@
|
||||
"""create pyramid table
|
||||
|
||||
Revision ID: 37c302fcd3c3
|
||||
Revises: c0b336d0d88a
|
||||
Create Date: 2026-03-30 21:32:57.704245
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from pgvector.sqlalchemy import Vector
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '37c302fcd3c3'
|
||||
down_revision: Union[str, Sequence[str], None] = 'c0b336d0d88a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.create_table(
|
||||
'pyramiddata',
|
||||
sa.Column('pyrdata_id', sa.BigInteger, primary_key=True),
|
||||
sa.Column('img_id', sa.Integer, nullable=False),
|
||||
sa.Column('level', sa.Integer, nullable=False),
|
||||
sa.Column('tilex', sa.Integer, nullable=False),
|
||||
sa.Column('tiley', sa.Integer, nullable=False),
|
||||
sa.Column('x', sa.Integer, nullable=False),
|
||||
sa.Column('y', sa.Integer, nullable=False),
|
||||
sa.Column('x0', sa.Integer, nullable=False),
|
||||
sa.Column('y0', sa.Integer, nullable=False),
|
||||
sa.Column('w', sa.Integer, nullable=False),
|
||||
sa.Column('h', sa.Integer, nullable=False),
|
||||
sa.Column('hash', sa.Text, nullable=False),
|
||||
sa.Column('histogram', Vector(48), nullable=True),
|
||||
sa.Column('tile_data', Vector(3), nullable=True),
|
||||
sa.Column('block_data', Vector(27), nullable=True)
|
||||
)
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_table('pyramiddata')
|
||||
@@ -0,0 +1,52 @@
|
||||
"""create second-order table
|
||||
|
||||
Revision ID: 6762186cc97c
|
||||
Revises: 37c302fcd3c3
|
||||
Create Date: 2026-04-23 16:48:07.206738
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from pgvector.sqlalchemy import Vector
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '6762186cc97c'
|
||||
down_revision: Union[str, Sequence[str], None] = '37c302fcd3c3'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.create_table(
|
||||
'pyramiddata_2tiles',
|
||||
sa.Column('pyrdata_id', sa.BigInteger, primary_key=True),
|
||||
sa.Column('img_id', sa.Integer, nullable=False),
|
||||
sa.Column('level', sa.Integer, nullable=False),
|
||||
sa.Column('tilex_a', sa.Integer, nullable=False),
|
||||
sa.Column('tiley_a', sa.Integer, nullable=False),
|
||||
sa.Column('x_a', sa.Integer, nullable=False),
|
||||
sa.Column('y_a', sa.Integer, nullable=False),
|
||||
sa.Column('x0_a', sa.Integer, nullable=False),
|
||||
sa.Column('y0_a', sa.Integer, nullable=False),
|
||||
sa.Column('w_a', sa.Integer, nullable=False),
|
||||
sa.Column('h_a', sa.Integer, nullable=False),
|
||||
sa.Column('tilex_b', sa.Integer, nullable=False),
|
||||
sa.Column('tiley_b', sa.Integer, nullable=False),
|
||||
sa.Column('x_b', sa.Integer, nullable=False),
|
||||
sa.Column('y_b', sa.Integer, nullable=False),
|
||||
sa.Column('x0_b', sa.Integer, nullable=False),
|
||||
sa.Column('y0_b', sa.Integer, nullable=False),
|
||||
sa.Column('w_b', sa.Integer, nullable=False),
|
||||
sa.Column('h_b', sa.Integer, nullable=False),
|
||||
sa.Column('hash', sa.Text, nullable=False),
|
||||
sa.Column('tile_data', Vector(6), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_table('pyramiddata_2tiles')
|
||||
@@ -0,0 +1,46 @@
|
||||
"""create level3 separate table
|
||||
|
||||
Revision ID: f750c5d057a7
|
||||
Revises: 6762186cc97c
|
||||
Create Date: 2026-05-05 00:52:23.600420
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from pgvector.sqlalchemy import Vector
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f750c5d057a7'
|
||||
down_revision: Union[str, Sequence[str], None] = '6762186cc97c'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.create_table(
|
||||
'pyramiddata_level3',
|
||||
sa.Column('pyrdata_id', sa.BigInteger, primary_key=True),
|
||||
sa.Column('img_id', sa.Integer, nullable=False),
|
||||
sa.Column('level', sa.Integer, nullable=False),
|
||||
sa.Column('tilex', sa.Integer, nullable=False),
|
||||
sa.Column('tiley', sa.Integer, nullable=False),
|
||||
sa.Column('x', sa.Integer, nullable=False),
|
||||
sa.Column('y', sa.Integer, nullable=False),
|
||||
sa.Column('x0', sa.Integer, nullable=False),
|
||||
sa.Column('y0', sa.Integer, nullable=False),
|
||||
sa.Column('w', sa.Integer, nullable=False),
|
||||
sa.Column('h', sa.Integer, nullable=False),
|
||||
sa.Column('hash', sa.Text, nullable=False),
|
||||
sa.Column('histogram', Vector(48), nullable=True),
|
||||
sa.Column('tile_data', Vector(3), nullable=True),
|
||||
sa.Column('block_data', Vector(27), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_table('pyramiddata_level3')
|
||||
@@ -0,0 +1,8 @@
|
||||
# src/__init__.py
|
||||
|
||||
# Import key modules to make them available at the package level
|
||||
from .db import DBOps
|
||||
from .analysis import convert_mmap_to_csv
|
||||
from .config import LEVELS_CONFIG
|
||||
|
||||
__all__ = ['DBOps', 'convert_mmap_to_csv', 'LEVELS_CONFIG']
|
||||
@@ -0,0 +1,187 @@
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
import csv
|
||||
import uuid
|
||||
from typing import List, Tuple
|
||||
|
||||
from .db import DBConnection, DBOps
|
||||
|
||||
BATCH_SIZE = 250
|
||||
def run_analysis():
|
||||
"""
|
||||
Iterate over all images in the 'img/' directory and process them using the DBOps class.
|
||||
|
||||
This function processes each image in the 'img/' directory, initializes the DBOps,
|
||||
and performs the analysis for each image.
|
||||
"""
|
||||
img_folder = "img"
|
||||
if not os.path.exists(img_folder):
|
||||
print(f"Error: The folder '{img_folder}' does not exist.")
|
||||
return
|
||||
|
||||
# Get all subdirectories in the 'img/' folder
|
||||
subdirectories = [
|
||||
d for d in os.listdir(img_folder)
|
||||
if os.path.isdir(os.path.join(img_folder, d))
|
||||
]
|
||||
|
||||
if not subdirectories:
|
||||
print(f"No subdirectories found in the folder '{img_folder}'.")
|
||||
return
|
||||
|
||||
print(f"Found {len(subdirectories)} subdirectories in the folder '{img_folder}'.")
|
||||
|
||||
# Initialize the database connection and operations
|
||||
db_conn = DBConnection('testdb')
|
||||
db_ops = DBOps(db_conn)
|
||||
|
||||
for subdir in subdirectories:
|
||||
# subdirs = ["TESTIMG", "IMG_9997", "IMG_9998", "IMG_9999", "IMG_2327"]
|
||||
# for subdir in subdirs:
|
||||
subdir_path = os.path.join(img_folder, subdir)
|
||||
# Look for the image file in the subdirectory
|
||||
|
||||
img_file = f"{subdir}.jpeg"
|
||||
img_path = os.path.join(subdir_path, img_file)
|
||||
print(f"\nProcessing image: {img_path}")
|
||||
|
||||
try:
|
||||
# Process the image analysis
|
||||
db_ops.process_image_analysis(img_path)
|
||||
print(f"Successfully processed image: {img_path}")
|
||||
except Exception as e:
|
||||
print(f"Error processing image {img_path}: {e}")
|
||||
|
||||
print("\nAnalysis of all images completed successfully.")
|
||||
|
||||
def convert_mmap_to_csv(mmap_file_path: str, csv_file_path: str, rows: int = 81) -> None:
|
||||
"""
|
||||
Convert a memory-mapped file to a CSV file.
|
||||
|
||||
Args:
|
||||
mmap_file_path (str): Path to the memory-mapped file.
|
||||
csv_file_path (str): Path to the output CSV file.
|
||||
rows (int): Number of rows in the memory-mapped data.
|
||||
"""
|
||||
# Load the memory-mapped data
|
||||
mmap_data = np.memmap(mmap_file_path, mode='r', dtype='float32', shape=(rows, 48))
|
||||
|
||||
# Convert the data to a list of lists for CSV writing
|
||||
data_for_csv = mmap_data.tolist()
|
||||
|
||||
# Write the data to a CSV file
|
||||
with open(csv_file_path, 'w', newline='') as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerows(data_for_csv)
|
||||
|
||||
def insert_tile_data(self, rows: List[Tuple]) -> None:
|
||||
"""
|
||||
Insert tile data into the pyramiddata table using batch insertion.
|
||||
|
||||
Args:
|
||||
rows (List[Tuple]): List of tuples containing tile data to insert.
|
||||
"""
|
||||
query_str = """
|
||||
INSERT INTO pyramiddata (
|
||||
img_id, level, tilex, tiley, x, y, x0, y0, w, h, hash, tile_data
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
print(len(rows))
|
||||
|
||||
with self._connection.get_cursor() as cursor:
|
||||
# Use execute_batch for efficient batch insertion
|
||||
extras.execute_batch(cursor, query_str, rows, page_size=BATCH_SIZE)
|
||||
extras.commit()
|
||||
|
||||
# def process_and_insert_tile_data_batched(
|
||||
# self,
|
||||
# img_id: int,
|
||||
# img_dims: Tuple[int, int],
|
||||
# level: int,
|
||||
# tile_file: str
|
||||
# ) -> None:
|
||||
# """
|
||||
# Process and insert tile data for a specific level of an image.
|
||||
# Args:
|
||||
# img_id (int): ID of the image.
|
||||
# img_dims (Tuple[int, int]): Dimensions of the image (width, height).
|
||||
# level (int): Pyramid level.
|
||||
# tile_file (str): The memory-mapped tile file.
|
||||
# """
|
||||
# # Calculate the downsampled resolution for this level
|
||||
# resolution = 2 ** level
|
||||
# img_w, img_h = img_dims
|
||||
|
||||
# # Load the memory-mapped tile data
|
||||
# tile_data = self.load_memmapped_tile_data(tile_file)
|
||||
|
||||
# print(f"img dims: {img_dims}")
|
||||
# print(f"tile data shape: {tile_data.shape}")
|
||||
|
||||
# tilew = img_w // resolution
|
||||
# tileh = img_h // resolution
|
||||
# width_t = img_w // tilew
|
||||
# height_t = img_h // tileh
|
||||
|
||||
# grid_size = 1
|
||||
|
||||
# def gen_tiles():
|
||||
# counter = 0
|
||||
# # Loop through each tile
|
||||
# for tiley in range(tileh):
|
||||
# for tilex in range(tilew):
|
||||
# x0 = tilex * width_t
|
||||
# y0 = tiley * height_t
|
||||
# w = grid_size
|
||||
# h = 1
|
||||
|
||||
# try:
|
||||
# tiledata = list(
|
||||
# (
|
||||
# np.r_[
|
||||
# tile_data[
|
||||
# (counter * 3):
|
||||
# ((counter + 1) * 3)
|
||||
# ]
|
||||
# ]
|
||||
# ).flatten().tolist()
|
||||
# )
|
||||
# counter += 1
|
||||
# yield (
|
||||
# img_id,
|
||||
# level,
|
||||
# tilex,
|
||||
# tiley,
|
||||
# width_t,
|
||||
# height_t,
|
||||
# x0,
|
||||
# y0,
|
||||
# w,
|
||||
# h,
|
||||
# str(uuid.uuid4()),
|
||||
# list(map(self.to_unsigned, tiledata))
|
||||
# )
|
||||
# except Exception as e:
|
||||
# print(f"ERROR: {e}")
|
||||
# counter += 1
|
||||
|
||||
# all_for_insertion = list(gen_tiles())
|
||||
# print(f"Inserting {len(all_for_insertion)} tiles")
|
||||
|
||||
# # # Batch the data for insertion
|
||||
# # batched = [
|
||||
# # all_for_insertion[BATCH_SIZE * i: BATCH_SIZE * (i + 1)]
|
||||
# # for i in range(int(len(all_for_insertion) / BATCH_SIZE) + 1)
|
||||
# # ]
|
||||
# # print(f"Inserting {len(batched)} batches")
|
||||
|
||||
# # for batch in batched:
|
||||
# # if batch: # Ensure the batch is not empty
|
||||
# # self.insert_tile_data(batch)
|
||||
|
||||
# self.insert_tile_data(all_for_insertion)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_analysis()
|
||||
@@ -0,0 +1,23 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: avdb-py-v1
|
||||
Version: 0.1.0
|
||||
Summary: A Python package for image analysis and database operations
|
||||
Home-page: https://github.com/yourusername/avdb-py-v1
|
||||
Author: Your Name
|
||||
Author-email: your.email@example.com
|
||||
Requires-Python: >=3.8
|
||||
Requires-Dist: psycopg2-binary>=2.9.6
|
||||
Requires-Dist: numpy>=1.21.0
|
||||
Requires-Dist: opencv-python>=4.5.5
|
||||
Provides-Extra: dev
|
||||
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
||||
Requires-Dist: black>=22.0.0; extra == "dev"
|
||||
Requires-Dist: flake8>=4.0.0; extra == "dev"
|
||||
Requires-Dist: mypy>=0.900; extra == "dev"
|
||||
Dynamic: author
|
||||
Dynamic: author-email
|
||||
Dynamic: home-page
|
||||
Dynamic: provides-extra
|
||||
Dynamic: requires-dist
|
||||
Dynamic: requires-python
|
||||
Dynamic: summary
|
||||
@@ -0,0 +1,10 @@
|
||||
README.md
|
||||
setup.py
|
||||
avdb/avdb_py_v1.egg-info/PKG-INFO
|
||||
avdb/avdb_py_v1.egg-info/SOURCES.txt
|
||||
avdb/avdb_py_v1.egg-info/dependency_links.txt
|
||||
avdb/avdb_py_v1.egg-info/requires.txt
|
||||
avdb/avdb_py_v1.egg-info/top_level.txt
|
||||
tests/test_alembic_migration.py
|
||||
tests/test_color_features_pyramid.py
|
||||
tests/test_db.py
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
psycopg2-binary>=2.9.6
|
||||
numpy>=1.21.0
|
||||
opencv-python>=4.5.5
|
||||
|
||||
[dev]
|
||||
pytest>=7.0.0
|
||||
black>=22.0.0
|
||||
flake8>=4.0.0
|
||||
mypy>=0.900
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Color Features Pyramid
|
||||
Part of [imgdb] - Tools for analysis, indexing, search, and creative use of image data
|
||||
|
||||
Overview
|
||||
========
|
||||
Pyramid down-sampling + color-feature analysis.
|
||||
Saves downsampled data to NAME.COLORSPACE.PYR{DOWNSAMPLING} data files mem-mapped to np arrays.
|
||||
Also retrieves and displays down-sampled image data.
|
||||
|
||||
ds res(w*h) gridpx(w,h) grid()
|
||||
|
||||
1 1920 * 1600 30, 20 64, 80
|
||||
2 32, 40
|
||||
3 480 * 400 16, 20
|
||||
4 240 * 200 15, 10 16, 20
|
||||
5 8, 10
|
||||
6 4, 5
|
||||
|
||||
|
||||
|
||||
Cols, Rows (Many are in portrait, same but with vertical window rects)
|
||||
Resolution 1280, 960
|
||||
No. Tiles 40, 40
|
||||
|
||||
Resolution 960, 1280
|
||||
No. Tiles 40, 40
|
||||
|
||||
|
||||
1280 960 0 1228800
|
||||
|
||||
640 480 1 307200
|
||||
320 240 2 76800
|
||||
160 120 3 19200 + HISTO
|
||||
80 60 4 4800 + HISTO
|
||||
40 30 5 1200 + HISTO
|
||||
20 15 6 300
|
||||
10 8 7 80
|
||||
5 4 8 20
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import math
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
# import matplotlib.pyplot as plt
|
||||
from config import (
|
||||
check_params,
|
||||
NUM_LEVELS,
|
||||
NUM_HISTO_BINS,
|
||||
LEVELS_CONFIG
|
||||
)
|
||||
import color_features_tile as cft
|
||||
|
||||
class ColorFeaturesPyramid(object):
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, **params) -> None:
|
||||
self.uuid = check_params(params, 'uuid', 'IMG_1001')
|
||||
# print(f"UUID: {self.uuid} -- {params}")
|
||||
self.full_tile = cft.ColorFeaturesTile(**{"uuid": self.uuid})
|
||||
self.down_sampling_levels = check_params(params, 'down_sampling', NUM_LEVELS)
|
||||
self.colorspace = check_params(params, 'colorspace', 'lab')
|
||||
self.write_flag = True
|
||||
self.debug = True
|
||||
|
||||
def _determine_num_bins(self, sz, lvl):
|
||||
return int(sz / math.pow(2, (lvl + 1)))
|
||||
|
||||
def _zeros_for_shape_level(self, sz, lvl):
|
||||
"""
|
||||
lists of np arrays of the correct size to hold our gradually shrinking arrays
|
||||
"""
|
||||
imgw, imgh = sz
|
||||
return np.zeros(
|
||||
(
|
||||
self._determine_num_bins(imgh, lvl),
|
||||
self._determine_num_bins(imgw, lvl),
|
||||
3,
|
||||
),
|
||||
dtype=np.uint16,
|
||||
)
|
||||
|
||||
def _down_sample_in_place(self, in_mtrx, pwr, out_shape): # pwr: 0 1 2 3 4 ...
|
||||
print(f"in_mtrx.shape {in_mtrx.shape}")
|
||||
print(f"out_shape {out_shape}")
|
||||
(a, b, _) = in_mtrx.shape
|
||||
out_mtrx = np.zeros(out_shape, dtype=in_mtrx.dtype)
|
||||
mult = 2 ** pwr # 1 2 4 8 16 ...
|
||||
# same shape
|
||||
|
||||
# for i in range(a):
|
||||
# for j in range(b):
|
||||
# print(i, j, a, b, in_mtrx[i,j,:])
|
||||
|
||||
for i in range(a):
|
||||
for j in range(b):
|
||||
# print(
|
||||
# (i * mult),
|
||||
# ((i + 1) * mult),
|
||||
# (j * mult),
|
||||
# ((j + 1) * mult),
|
||||
# i,
|
||||
# j,
|
||||
# a,
|
||||
# b,
|
||||
# in_mtrx[i, j, :],
|
||||
# )
|
||||
out_mtrx[
|
||||
(i * mult) : ((i + 1) * mult), (j * mult) : ((j + 1) * mult)
|
||||
] = in_mtrx[i,j,:]
|
||||
|
||||
return out_mtrx
|
||||
|
||||
def _down_sample_and_analyze(self):
|
||||
"""
|
||||
Reads and image, performs a series of down-samplings, writes each to disk
|
||||
"""
|
||||
# Validate colorspace
|
||||
valid_colorspaces = ["rgb", "lab", "lab"]
|
||||
if self.colorspace not in valid_colorspaces:
|
||||
raise ValueError(f"Invalid colorspace: {self.colorspace}. Valid options are: {valid_colorspaces}")
|
||||
|
||||
# dataroot = f'{self.full_tile.root_path}/img/'
|
||||
datapath = f'{self.full_tile.root_path}/img/{self.full_tile.uuid}/{self.full_tile.uuid}.jpeg'
|
||||
print(f'Down-sample + analyze: {datapath} - ({str(self.down_sampling_levels)})')
|
||||
|
||||
img = cv.imread(datapath)
|
||||
|
||||
print(img.shape)
|
||||
# print(img[500:520,600:620,:])
|
||||
|
||||
imgw = img.shape[1]
|
||||
imgh = img.shape[0]
|
||||
ds_levels = int(self.down_sampling_levels)
|
||||
|
||||
# convert color space once
|
||||
if self.colorspace == "rgb":
|
||||
img_ = cv.cvtColor(img, cv.COLOR_BGR2RGB)
|
||||
elif self.colorspace == "hsv":
|
||||
img_ = cv.cvtColor(img, cv.COLOR_BGR2HSV)
|
||||
else:
|
||||
img_ = cv.cvtColor(img, cv.COLOR_BGR2Lab)
|
||||
|
||||
# add 1 to # of levels so we can write to the Nth + 1 level, properly sized
|
||||
# 0th level is image itself in imgs_ds, concat shifts zeros arrays to start
|
||||
# at index 1
|
||||
imgs_ds = [img_] + [
|
||||
self._zeros_for_shape_level((imgw, imgh), level)
|
||||
for level in range(ds_levels)
|
||||
]
|
||||
print(f"imgs_ds len: {len(imgs_ds)}\n")
|
||||
for i,img in enumerate(imgs_ds):
|
||||
print(f"{i}: {img.shape}\n")
|
||||
|
||||
base_shape = imgs_ds[0].shape
|
||||
|
||||
try:
|
||||
for level in range(ds_levels - 1):
|
||||
imgs_ds[(level + 1)] = cv.pyrDown(imgs_ds[level], imgs_ds[(level + 1)])
|
||||
|
||||
# display copy
|
||||
img_a = self._down_sample_in_place(
|
||||
imgs_ds[level],
|
||||
level,
|
||||
base_shape
|
||||
)
|
||||
print(f"img_a shape: {img_a.shape} - imgs_ds shape: {imgs_ds[(level + 1)].shape} - level+1:{level+1}\n")
|
||||
print(f"writing {datapath[:-5]}.pyr_{str(level + 1)}_display.jpeg\n")
|
||||
dsimg_filename = f"{datapath[:-5]}.{self.colorspace}.pyr_{str(level)}_display.jpeg"
|
||||
if True or not os.path.exists(dsimg_filename):
|
||||
cv.imwrite(dsimg_filename, img_a)
|
||||
|
||||
# real copy
|
||||
img_b = self._down_sample_in_place(
|
||||
imgs_ds[level],
|
||||
0,
|
||||
imgs_ds[level].shape
|
||||
)
|
||||
print(f"img_b shape: {img_b.shape} - imgs_ds shape: {imgs_ds[(level + 1)].shape} - level+1:{level+1}\n")
|
||||
print(f"writing {datapath[:-5]}.pyr_{str(level + 1)}.jpeg\n")
|
||||
dsimg_filename = f"{datapath[:-5]}.{self.colorspace}.pyr_{str(level)}.jpeg"
|
||||
if True or not os.path.exists(dsimg_filename):
|
||||
cv.imwrite(dsimg_filename, img_b)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
pass
|
||||
|
||||
# TODO: split out and refactor to allow different data outputs
|
||||
# for level in range(ds_levels):
|
||||
# fp = np.memmap((datapath + str(level+1)), dtype='uint8', mode='w+',
|
||||
# shape=(imgs_ds[level+1].shape[0], imgs_ds[level+1].shape[1], 3))
|
||||
# fp[:] = np.copy(imgs_ds[level+1])
|
||||
# fp.flush()
|
||||
|
||||
# for now we need the 0th, original img AND
|
||||
# all the ds_levels recursively down-sampled image files
|
||||
# + 1 to include the 0th level
|
||||
for level in range(ds_levels - 1):
|
||||
# sub_img = imgs_ds[level]
|
||||
|
||||
feat_tiles = cft.ColorFeaturesTile(
|
||||
**{
|
||||
"uuid": self.uuid,
|
||||
"level": int(level),
|
||||
"root_path": self.full_tile.root_path
|
||||
}
|
||||
)
|
||||
if LEVELS_CONFIG[level]["gridsplit"] is not None:
|
||||
gridsplit = LEVELS_CONFIG[level]["gridsplit"]
|
||||
yield (
|
||||
str(level),
|
||||
feat_tiles.analyze_img_histograms_postnorm(level)
|
||||
)
|
||||
if LEVELS_CONFIG[level]["tiles"]:
|
||||
yield (
|
||||
str(level),
|
||||
feat_tiles.analyze_img_tiles_postnorm(level)
|
||||
)
|
||||
if LEVELS_CONFIG[level]["target"]:
|
||||
targetblocks = LEVELS_CONFIG[level]["targetblocks"]
|
||||
yield (
|
||||
str(level),
|
||||
feat_tiles.analyze_img_blocks_postnorm(level)
|
||||
)
|
||||
@@ -0,0 +1,261 @@
|
||||
# color_features_tiles.py - color features tiled data from images
|
||||
# ImageDB2 - Tools for analysis, indexing, search, and creative use of image data
|
||||
|
||||
"""
|
||||
ColorFeaturesTiles
|
||||
Part of ImageDB2 - Tools for analysis, indexing, search, and creative use of image data
|
||||
|
||||
Overview
|
||||
========
|
||||
Pyramid down-sampling + color-feature analysis.
|
||||
Saves downsampled data to NAME.COLORSPACE.TILE{SIZE}.OLAP{SIZE} data files mem-mapped to np arrays.
|
||||
Also retrieves and displays tiled image data.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import math
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
|
||||
from avdb.config import (
|
||||
check_params,
|
||||
NUM_HISTO_BINS,
|
||||
LEVELS_CONFIG
|
||||
)
|
||||
|
||||
|
||||
class ColorFeaturesTile(object):
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, **params) -> None:
|
||||
self.uuid = check_params(params, 'uuid', 'TESTIMG')
|
||||
self.root_path = check_params(params, 'root_path', '/home/kfl/dev/git/avdb-py-v1')
|
||||
self.level = check_params(params, 'level', 0)
|
||||
self.colorspace = check_params(params, 'color_space', 'lab')
|
||||
self.write_flag = True
|
||||
self.debug = True
|
||||
print(f"CREATING TILE -- LEVEL: {self.level} - UUID: {self.uuid} color space: {self.colorspace} - write_flag: {self.write_flag}")
|
||||
|
||||
def analyze_img_tiles_postnorm(self, lvl=None):
|
||||
"""
|
||||
"""
|
||||
if lvl is None:
|
||||
imgpath = f"{self.root_path}/img/{self.uuid}/{self.uuid}.jpeg"
|
||||
else:
|
||||
imgpath = f"{self.root_path}/img/{self.uuid}/{self.uuid}.{self.colorspace}.pyr_{lvl}.jpeg"
|
||||
|
||||
print(imgpath)
|
||||
img = cv.imread(imgpath, cv.IMREAD_UNCHANGED)
|
||||
imgw = img.shape[1]
|
||||
imgh = img.shape[0]
|
||||
|
||||
# iterate over the tiles and unroll into linear chunks
|
||||
def _aux():
|
||||
for top_crnr in range(imgh):
|
||||
for lft_crnr in range(imgw):
|
||||
# print(img[top_crnr, lft_crnr])
|
||||
yield np.r_[
|
||||
img[top_crnr,lft_crnr],
|
||||
]
|
||||
|
||||
total_tiles = np.c_[list(_aux())]
|
||||
|
||||
if self.write_flag is True:
|
||||
print(imgpath)
|
||||
mmappath = f"{imgpath}.tiles.mmap"
|
||||
|
||||
fp = np.memmap(
|
||||
mmappath,
|
||||
dtype=np.uint16,
|
||||
mode="w+",
|
||||
shape=total_tiles.shape
|
||||
)
|
||||
print(f"all tiles : {total_tiles.shape}")
|
||||
fp[:] = total_tiles
|
||||
fp.flush()
|
||||
|
||||
return total_tiles, (imgw, imgh, 1, 1)
|
||||
|
||||
def analyze_img_blocks_postnorm(self, lvl=None):
|
||||
"""
|
||||
"""
|
||||
if lvl is None:
|
||||
imgpath = f"{self.root_path}/img/{self.uuid}/{self.uuid}.jpeg"
|
||||
else:
|
||||
imgpath = f"{self.root_path}/img/{self.uuid}/{self.uuid}.{self.colorspace}.pyr_{lvl}.jpeg"
|
||||
|
||||
print(imgpath)
|
||||
img = cv.imread(imgpath, cv.IMREAD_UNCHANGED)
|
||||
imgw = img.shape[1]
|
||||
imgh = img.shape[0]
|
||||
|
||||
target_blocks = LEVELS_CONFIG[lvl]["targetblocks"]
|
||||
|
||||
if target_blocks is None:
|
||||
raise ValueError(f"target_blocks is None for level {self.level}")
|
||||
|
||||
# calculate gutters
|
||||
imgw -= 2
|
||||
imgh -= 2
|
||||
|
||||
# iterate over the blocks and unroll into linear chunks
|
||||
def _aux():
|
||||
for top_crnr in range(imgh):
|
||||
for lft_crnr in range(imgw):
|
||||
yield np.r_[
|
||||
img[top_crnr,lft_crnr:lft_crnr+3],
|
||||
img[top_crnr+1,lft_crnr:lft_crnr+3],
|
||||
img[top_crnr+2,lft_crnr:lft_crnr+3]
|
||||
].flatten()
|
||||
|
||||
total_blocks = np.c_[list(_aux())]
|
||||
|
||||
if self.write_flag is True:
|
||||
print(imgpath)
|
||||
mmappath = f"{imgpath}.blocks.mmap"
|
||||
|
||||
fp = np.memmap(
|
||||
mmappath,
|
||||
dtype=np.uint16,
|
||||
mode="w+",
|
||||
shape=total_blocks.shape
|
||||
)
|
||||
print(f"all histograms : {total_blocks.shape}")
|
||||
fp[:] = total_blocks
|
||||
fp.flush()
|
||||
|
||||
return total_blocks, (imgw, imgh, 1, 1)
|
||||
|
||||
def analyze_img_histograms_postnorm(self, lvl=None):
|
||||
"""
|
||||
overlapping tiles analysis
|
||||
+ width in tiles must be a multiple of 2
|
||||
"""
|
||||
if lvl is None:
|
||||
imgpath = f"{self.root_path}/img/{self.uuid}/{self.uuid}.jpeg"
|
||||
else:
|
||||
imgpath = f"{self.root_path}/img/{self.uuid}/{self.uuid}.{self.colorspace}.pyr_{lvl}.jpeg"
|
||||
|
||||
print(imgpath)
|
||||
img = cv.imread(imgpath, cv.IMREAD_UNCHANGED)
|
||||
imgw = img.shape[1]
|
||||
imgh = img.shape[0]
|
||||
|
||||
# / by 1 = whole dimension
|
||||
gridsplit = LEVELS_CONFIG[lvl]["gridsplit"]
|
||||
tilesw = (gridsplit + 1) if gridsplit is not None else 1
|
||||
tilesh = (gridsplit + 1) if gridsplit is not None else 1
|
||||
|
||||
tilew = int(imgw / tilesw)
|
||||
tileh = int(imgh / tilesh)
|
||||
|
||||
# img = self.convert_colorspace(img)
|
||||
mask = np.zeros(img.shape[:2], np.uint8)
|
||||
|
||||
all_histograms = np.array([], dtype=np.float32)
|
||||
all_histograms.shape = (0, 1)
|
||||
|
||||
lastx, lasty = 0, 0
|
||||
|
||||
print(f"dims: {(tilesw,tilesh)}")
|
||||
|
||||
for y in range(tilesh):
|
||||
|
||||
for x in range(tilesw):
|
||||
|
||||
if (lastx, lasty) != (x, y):
|
||||
mask[
|
||||
int(tilew/2 * lastx) : int((tilew/2 * lastx) + tilew),
|
||||
int(tileh/2 * lasty) : int((tileh/2 * lasty) + tileh),
|
||||
] = 0
|
||||
|
||||
lastx = x
|
||||
lasty = y
|
||||
|
||||
mask[
|
||||
int(tilew/2 * x) : int((tilew/2 * x) + tilew),
|
||||
int(tileh/2 * y) : int((tileh/2 * y) + tileh),
|
||||
] = 255
|
||||
|
||||
# calculate 3 channels' histograms
|
||||
hist0_masked = cv.calcHist(
|
||||
[img], [0], mask, [NUM_HISTO_BINS], [0, 65536]
|
||||
)
|
||||
hist0_masked.shape = (NUM_HISTO_BINS, 1)
|
||||
row0 = hist0_masked
|
||||
|
||||
hist1_masked = cv.calcHist(
|
||||
[img], [1], mask, [NUM_HISTO_BINS], [0, 256]
|
||||
)
|
||||
hist1_masked.shape = (NUM_HISTO_BINS, 1)
|
||||
row1 = hist1_masked
|
||||
|
||||
hist2_masked = cv.calcHist(
|
||||
[img], [2], mask, [NUM_HISTO_BINS], [0, 256]
|
||||
)
|
||||
hist2_masked.shape = (NUM_HISTO_BINS, 1)
|
||||
row2 = hist2_masked
|
||||
|
||||
# print(f"row0: {row0} - {row0.shape}")
|
||||
# print(f"row1: {row1} - {row1.shape}")
|
||||
# print(f"row2: {row2} - {row2.shape}")
|
||||
|
||||
# L [bin0,bin1,..bin15], r=0, c=0
|
||||
# A [bin0,bin1,..bin15]
|
||||
# B [bin0,bin1,..bin15]
|
||||
|
||||
# L [bin0,bin1,..bin15], r=0, c=1
|
||||
# A [bin0,bin1,..bin15]
|
||||
# B [bin0,bin1,..bin15]
|
||||
|
||||
# etc.
|
||||
# top left to bottom right
|
||||
|
||||
all_histograms: np.array = np.r_[
|
||||
all_histograms, row0, row1, row2
|
||||
]
|
||||
|
||||
# print(f"ALL HISTOGRAMS: {all_histograms.shape}")
|
||||
# print(all_histograms[-48:,:])
|
||||
|
||||
total_hist = np.reshape(all_histograms[:], (-1,48))
|
||||
|
||||
sz = tilesh * tilesw
|
||||
|
||||
cv.normalize(
|
||||
total_hist, total_hist, norm_type=cv.NORM_MINMAX
|
||||
)
|
||||
|
||||
# total_hist *= 255.
|
||||
|
||||
print(f"total HISTOGRAM: {total_hist.shape} {total_hist.sum()}")
|
||||
|
||||
print(total_hist[:2,:])
|
||||
print(np.max(total_hist, 0)[:24])
|
||||
print(np.max(total_hist, 1)[:24])
|
||||
|
||||
print(np.min(total_hist, 0)[:24])
|
||||
print(np.min(total_hist, 1)[:24])
|
||||
|
||||
print(np.mean(total_hist, 0)[:24])
|
||||
print(np.mean(total_hist, 1)[:24])
|
||||
|
||||
if self.write_flag is True:
|
||||
print(f"writing to: {imgpath}.histos.mmap")
|
||||
mmappath = f"{imgpath}.histos.mmap"
|
||||
|
||||
fp = np.memmap(
|
||||
mmappath,
|
||||
dtype="float32",
|
||||
mode="w+",
|
||||
shape=total_hist.shape
|
||||
)
|
||||
print(f"all histograms : {total_hist.shape}")
|
||||
fp[:] = total_hist
|
||||
# print(f"CHECK: {fp[:2,:]} - {np.max(fp)} - {np.min(fp)}")
|
||||
fp.flush()
|
||||
|
||||
return total_hist, (imgw, imgh, tilesw, tilesh)
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
|
||||
NUM_LEVELS = 9
|
||||
NUM_HISTO_BINS = 16
|
||||
NUM_ID_BINS = 5 # id, x, y, tilew, tileh
|
||||
|
||||
|
||||
def check_params(params, k, defaultval):
|
||||
if k in params.keys():
|
||||
return params[k]
|
||||
else:
|
||||
return defaultval
|
||||
|
||||
|
||||
LEVELS_CONFIG = [
|
||||
{
|
||||
"level": 0,
|
||||
"tiles": False,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
},
|
||||
{
|
||||
"level": 1,
|
||||
"tiles": False,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
},
|
||||
{
|
||||
"level": 2,
|
||||
"tiles": False,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
},
|
||||
{
|
||||
"level": 3,
|
||||
"tiles": True,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
},
|
||||
{
|
||||
"level": 4,
|
||||
"tiles": True,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
},
|
||||
{
|
||||
"level": 5,
|
||||
"tiles": True,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
},
|
||||
{
|
||||
"level": 6,
|
||||
"tiles": True,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
},
|
||||
{
|
||||
"level": 7,
|
||||
"tiles": True,
|
||||
"gridsplit": None,
|
||||
"target": False,
|
||||
"targetblocks": None
|
||||
}
|
||||
]
|
||||
+679
@@ -0,0 +1,679 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import uuid
|
||||
import csv
|
||||
from typing import List, Tuple
|
||||
import psycopg2
|
||||
from psycopg2 import extras
|
||||
import numpy as np
|
||||
import cv2 as cv
|
||||
|
||||
from avdb.config import LEVELS_CONFIG
|
||||
|
||||
BATCH_SIZE=200
|
||||
|
||||
class DBConnection:
|
||||
def __init__(self, dbname='testdb'):
|
||||
try:
|
||||
self._connection = psycopg2.connect(
|
||||
user="postgres",
|
||||
password='pgRocks',
|
||||
host="127.0.0.1",
|
||||
port="5432",
|
||||
database=dbname
|
||||
)
|
||||
print(f"\nconnection: {self._connection}\n\n")
|
||||
self._connection.autocommit = True
|
||||
self.dbname = dbname
|
||||
# Create a cursor to execute queries
|
||||
self.cursor = self._connection.cursor()
|
||||
|
||||
# Print PostgreSQL details
|
||||
print("PostgreSQL server information")
|
||||
print(self._connection.get_dsn_parameters(), "\n")
|
||||
|
||||
# Executing a SQL query
|
||||
self.cursor.execute("SELECT version();")
|
||||
|
||||
# Fetch result
|
||||
record = self.cursor.fetchone()
|
||||
print("You are connected to - ", record, "\n")
|
||||
|
||||
except (Exception, psycopg2.Error) as error:
|
||||
print("Error while connecting to PostgreSQL", error)
|
||||
|
||||
def get_cursor(self):
|
||||
return self._connection.cursor()
|
||||
|
||||
def get_connection(self):
|
||||
return self._connection
|
||||
|
||||
def test_db_connection():
|
||||
"""
|
||||
Test if the database is active and available by attempting a connection and a simple query.
|
||||
|
||||
Returns:
|
||||
bool: True if the database connection is successful, False otherwise.
|
||||
"""
|
||||
try:
|
||||
# Attempt to establish a connection
|
||||
conn = DBConnection('testdb')
|
||||
cursor = conn.get_cursor()
|
||||
|
||||
# Execute a simple query to verify the connection
|
||||
cursor.execute("SELECT 1;")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result[0] == 1:
|
||||
print("Database connection test successful!")
|
||||
return True
|
||||
else:
|
||||
print("Database connection test failed.")
|
||||
return False
|
||||
|
||||
except (Exception, psycopg2.Error) as error:
|
||||
print(f"Error while testing database connection: {error}")
|
||||
return False
|
||||
|
||||
class DBOps:
|
||||
def __init__(self, connection):
|
||||
# self._dbname = connection.dbname
|
||||
self._connection = connection
|
||||
|
||||
def insert_image(
|
||||
self,
|
||||
img_path: str,
|
||||
img_name: str,
|
||||
width: int,
|
||||
height: int,
|
||||
img_id= None
|
||||
) -> int:
|
||||
"""
|
||||
Insert a new image record into the images table.
|
||||
|
||||
Args:
|
||||
img_path (str): Path to the image file.
|
||||
img_name (str): Name of the image file.
|
||||
width (int): Width of the image in pixels.
|
||||
height (int): Height of the image in pixels.
|
||||
|
||||
Returns:
|
||||
int: The ID of the newly inserted image record.
|
||||
"""
|
||||
print(f"Inserting image: {img_path},{img_id}")
|
||||
query_str = """
|
||||
INSERT INTO avdb_image (img_path, img_name, width, height)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING img_id;
|
||||
"""
|
||||
with self._connection.get_cursor() as cursor:
|
||||
cursor.execute(query_str, (img_path, img_name, width, height))
|
||||
img_id_out = cursor.fetchone()[0]
|
||||
print(f"Warning if true: {img_id != img_id_out}")
|
||||
return img_id_out
|
||||
|
||||
def insert_histogram_data(
|
||||
self,
|
||||
img_id: int,
|
||||
level: int,
|
||||
tilex: int,
|
||||
tiley: int,
|
||||
x: int,
|
||||
y: int,
|
||||
x0: int,
|
||||
y0: int,
|
||||
w: int,
|
||||
h: int,
|
||||
histogram_data: List[float]
|
||||
) -> int:
|
||||
"""
|
||||
Insert histogram data into the histogramfeatures table.
|
||||
|
||||
Args:
|
||||
img_id (int): ID of the image.
|
||||
level (int): Pyramid level.
|
||||
tilex (int): Logical tile x-coordinate.
|
||||
tiley (int): Logical tile y-coordinate.
|
||||
x (int): Upper left x-coordinate (in original resolution).
|
||||
y (int): Upper left y-coordinate (in original resolution).
|
||||
x0 (int): Upper left x-coordinate (in down-sampled resolution).
|
||||
y0 (int): Upper left y-coordinate (in down-sampled resolution).
|
||||
w (int): Width of the histogram region.
|
||||
h (int): Height of the histogram region.
|
||||
histogram_data (np.ndarray): Histogram data as a numpy array.
|
||||
[tile_data: NULL.]
|
||||
|
||||
Returns:
|
||||
int: The ID of the newly inserted histogram record.
|
||||
"""
|
||||
query_str = """
|
||||
INSERT INTO pyramiddata (
|
||||
img_id, level, tilex, tiley, x, y, x0, y0, w, h, hash, histogram
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING pyrdata_id;
|
||||
"""
|
||||
hsh = uuid.uuid4()
|
||||
with self._connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
query_str,
|
||||
(
|
||||
img_id,
|
||||
level,
|
||||
tilex,
|
||||
tiley,
|
||||
x,
|
||||
y,
|
||||
x0,
|
||||
y0,
|
||||
w,
|
||||
h,
|
||||
str(hsh),
|
||||
histogram_data
|
||||
)
|
||||
)
|
||||
try:
|
||||
pyrdata_id = cursor.fetchone()[0]
|
||||
except Exception as e:
|
||||
print(f"EXEPTION: {e}")
|
||||
return pyrdata_id
|
||||
|
||||
def insert_block_data(
|
||||
self,
|
||||
img_id: int,
|
||||
level: int,
|
||||
tilex: int,
|
||||
tiley: int,
|
||||
x: int,
|
||||
y: int,
|
||||
x0: int,
|
||||
y0: int,
|
||||
w: int,
|
||||
h: int,
|
||||
block_data: np.ndarray
|
||||
) -> int:
|
||||
"""
|
||||
Insert block data into the tilefeatures table.
|
||||
|
||||
Args:
|
||||
img_id (int): ID of the image.
|
||||
level (int): Pyramid level.
|
||||
tilex (int): Logical tile x-coordinate.
|
||||
tiley (int): Logical tile y-coordinate.
|
||||
x (int): Center x-coordinate (in original resolution).
|
||||
y (int): Center y-coordinate (in original resolution).
|
||||
x0 (int): Center x-coordinate (in down-sampled resolution).
|
||||
y0 (int): Center y-coordinate (in down-sampled resolution).
|
||||
w (int): Width of the tile.
|
||||
h (int): Height of the tile.
|
||||
block_data (np.ndarray): Tile data as a numpy array.
|
||||
[histogram_data: NULL.]
|
||||
|
||||
Returns:
|
||||
int: The ID of the newly inserted tile record.
|
||||
"""
|
||||
query_str = """
|
||||
INSERT INTO pyramiddata (
|
||||
img_id, level, tilex, tiley, x, y, x0, y0, w, h, hash, block_data
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING pyrdata_id;
|
||||
|
||||
"""
|
||||
hsh = uuid.uuid4()
|
||||
with self._connection.get_cursor() as cursor:
|
||||
cursor.execute(
|
||||
query_str,
|
||||
(
|
||||
img_id,
|
||||
level,
|
||||
tilex,
|
||||
tiley,
|
||||
x,
|
||||
y,
|
||||
x0,
|
||||
y0,
|
||||
w,
|
||||
h,
|
||||
str(hsh),
|
||||
block_data
|
||||
)
|
||||
)
|
||||
pyrdata_id = cursor.fetchone()[0]
|
||||
return pyrdata_id
|
||||
|
||||
def insert_tile_data(self, rows: List[Tuple]) -> None:
|
||||
"""
|
||||
Insert tile data into the pyramiddata table using batch insertion.
|
||||
|
||||
Args:
|
||||
rows (List[Tuple]): List of tuples containing tile data to insert.
|
||||
"""
|
||||
query_str = """
|
||||
INSERT INTO pyramiddata (
|
||||
img_id, level, tilex, tiley, x, y, x0, y0, w, h, hash, tile_data
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
with self._connection.cursor() as cursor:
|
||||
# Use execute_batch for efficient batch insertion
|
||||
print(f"Inserting {len(rows)} rows into pyramid {rows[:1]}")
|
||||
extras.execute_batch(cursor, query_str, rows)
|
||||
self._connection.commit()
|
||||
|
||||
def load_memmapped_histogram_data(self, file_path: str, rows=81):
|
||||
"""
|
||||
Load memory-mapped histogram data from a file.
|
||||
|
||||
Args:
|
||||
file_path (str): Path to the file containing the histogram data.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The loaded histogram data as a numpy array.
|
||||
"""
|
||||
return np.memmap(file_path, mode='r+', dtype='float32', shape=(rows,48))
|
||||
|
||||
def load_memmapped_tile_data(self, file_path: str):
|
||||
"""
|
||||
Load memory-mapped tile data from a file.
|
||||
|
||||
Args:
|
||||
file_path (str): Path to the file containing the tile data.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The loaded tile data as a numpy array.
|
||||
"""
|
||||
return np.memmap(file_path, mode='r+', dtype=np.uint16)
|
||||
|
||||
def load_memmapped_block_data(self, file_path: str):
|
||||
"""
|
||||
Load memory-mapped tile data from a file.
|
||||
|
||||
Args:
|
||||
file_path (str): Path to the file containing the tile data.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The loaded tile data as a numpy array.
|
||||
"""
|
||||
return np.memmap(file_path, mode='r+', dtype=np.uint16)
|
||||
|
||||
def get_image_by_id(self, img_id: int) -> Tuple:
|
||||
"""
|
||||
Retrieve an image record by its ID.
|
||||
|
||||
Args:
|
||||
img_id (int): ID of the image to retrieve.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple representing the image record.
|
||||
"""
|
||||
query_str = "SELECT * FROM images WHERE img_id = %s;"
|
||||
with self._connection.cursor() as cursor:
|
||||
cursor.execute(query_str, (img_id,))
|
||||
return cursor.fetchone()
|
||||
|
||||
def get_pyramid_metadata_by_image_id(self, img_id: int) -> List[Tuple]:
|
||||
"""
|
||||
Retrieve all pyramid metadata records associated with an image.
|
||||
|
||||
Args:
|
||||
img_id (int): ID of the image.
|
||||
|
||||
Returns:
|
||||
list: A list of tuples representing the pyramid metadata records.
|
||||
"""
|
||||
query_str = "SELECT * FROM pyramidmetadata WHERE img_id = %s;"
|
||||
with self._connection.cursor() as cursor:
|
||||
cursor.execute(query_str, (img_id,))
|
||||
return cursor.fetchall()
|
||||
|
||||
def get_pyramid_metadata_by_id(self, pyramid_id: int) -> Tuple:
|
||||
"""
|
||||
Retrieve a pyramid metadata record by its ID.
|
||||
|
||||
Args:
|
||||
pyramid_id (int): ID of the pyramid metadata record.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple representing the pyramid metadata record.
|
||||
"""
|
||||
query_str = "SELECT * FROM pyramidmetadata WHERE pyramid_id = %s;"
|
||||
with self._connection.cursor() as cursor:
|
||||
cursor.execute(query_str, (pyramid_id,))
|
||||
return cursor.fetchone()
|
||||
|
||||
def get_image_id_by_img_name(self, img_name: str) -> int:
|
||||
"""
|
||||
Retrieve the ID of an image by its name.
|
||||
Args:
|
||||
img_name (str): Name of the image file.
|
||||
|
||||
Returns:
|
||||
int: The ID of the image, or None if not found.
|
||||
"""
|
||||
query_str = "SELECT img_id FROM avdb_image WHERE img_name = %s;"
|
||||
with self._connection.cursor() as cursor:
|
||||
cursor.execute(query_str, (img_name,))
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else None
|
||||
|
||||
def process_and_insert_histogram_data(
|
||||
self,
|
||||
img_id: int,
|
||||
img_dims: Tuple[int, int],
|
||||
level: int,
|
||||
histogram_file: str,
|
||||
grid_size: int = 9,
|
||||
bin_size: int = 48
|
||||
) -> None:
|
||||
"""
|
||||
Process and insert histogram data for a specific level of an image.
|
||||
Args:
|
||||
img_id (int): ID of the image.
|
||||
img_dims (tuple): Dimensions of the image (width, height).
|
||||
level (int): Pyramid level.
|
||||
histogram_file (str): The memory-mapped histogram file.
|
||||
grid_size (int): Size of the grid (e.g., 9 for 9x9 grid).
|
||||
bin_size (int): Number of bins in the histogram (e.g., 48 for Lab histogram).
|
||||
"""
|
||||
if level<1:
|
||||
raise ValueError("Level must be greater than 0")
|
||||
# Calculate the downsampled resolution for this level
|
||||
resolution = 2 ** level
|
||||
|
||||
# Load the memory-mapped histogram data
|
||||
histogram_data = self.load_memmapped_histogram_data(histogram_file, grid_size**2)
|
||||
|
||||
# print(type(histogram_data))
|
||||
|
||||
print(f"histogram_data shape: {histogram_data.shape}")
|
||||
|
||||
img_w,img_h = img_dims
|
||||
|
||||
# print(list((histogram_data[0,:]).flatten()))
|
||||
counter = 0
|
||||
# Loop through each grid cell
|
||||
for tilex in range(grid_size):
|
||||
for tiley in range(grid_size):
|
||||
# Calculate spatial coordinates (simplified for now)
|
||||
# These calculations can be adjusted based on your specific requirements
|
||||
x = tilex * (img_w // resolution)
|
||||
y = tiley * (img_h // resolution)
|
||||
x0 = tilex * (img_w // (resolution * grid_size ))
|
||||
y0 = tiley * (img_h // (resolution * grid_size))
|
||||
w = img_w // (resolution / 2)
|
||||
h = img_h // (resolution / 2)
|
||||
# Insert the histogram data into the database
|
||||
# print(f"Inserting histogram data for tilex: {tilex}, tiley: {tiley}")
|
||||
# print(f"x={x}, y={y}, x0={x0}, y0={y0}, w={w}, h={h}")
|
||||
# print(type(list((histogram_data[counter,:]).flatten().tolist())[0]))
|
||||
self.insert_histogram_data(
|
||||
img_id=img_id,
|
||||
level=level,
|
||||
tilex=tilex,
|
||||
tiley=tiley,
|
||||
x=x,
|
||||
y=y,
|
||||
x0=x0,
|
||||
y0=y0,
|
||||
w=w,
|
||||
h=h,
|
||||
histogram_data=list((histogram_data[counter,:]).flatten().tolist())
|
||||
)
|
||||
counter += 1
|
||||
|
||||
def to_unsigned(self, signed_int):
|
||||
return signed_int + (1 << 8) if signed_int < 0 else signed_int
|
||||
|
||||
def process_and_insert_tile_data_batched(
|
||||
self,
|
||||
img_id: int,
|
||||
img_dims: Tuple[int, int],
|
||||
level: int,
|
||||
tile_file: str
|
||||
) -> None:
|
||||
"""
|
||||
Process and insert tile data for a specific level of an image.
|
||||
Args:
|
||||
img_id (int): ID of the image.
|
||||
img_dims (Tuple[int, int]): Dimensions of the image (width, height).
|
||||
level (int): Pyramid level.
|
||||
tile_file (str): The memory-mapped tile file.
|
||||
grid_size (int): Size of the grid.
|
||||
"""
|
||||
# Calculate the downsampled resolution for this level
|
||||
resolution = 2 ** level
|
||||
img_w, img_h = img_dims
|
||||
# Load the memory-mapped tile data
|
||||
print(tile_file)
|
||||
tile_data = self.load_memmapped_tile_data(tile_file)
|
||||
|
||||
print(f"img dims: {img_dims}")
|
||||
print(f"tile data shape:{tile_data.shape}")
|
||||
|
||||
tilew = img_w // resolution
|
||||
tileh = img_h // resolution
|
||||
width_t = img_w // tilew
|
||||
height_t = img_h // tileh
|
||||
|
||||
grid_size = 1
|
||||
|
||||
def gen_tiles():
|
||||
counter = 0
|
||||
# Loop through each tile
|
||||
for tiley in range(tileh):
|
||||
for tilex in range(tilew):
|
||||
|
||||
x0 = tilex * width_t
|
||||
y0 = tiley * height_t
|
||||
w = grid_size
|
||||
h = 1
|
||||
|
||||
# print(f"{counter} - {level} - {tilex} - {tiley} - {tilew} - {tileh} - {width_t} - {height_t} - {x0} - {y0} - {w} - {h} -- {tile_data[(counter * 3):((counter+1) * 3)]}")
|
||||
# Insert the tile data into the database
|
||||
try:
|
||||
tiledata = list(
|
||||
(
|
||||
np.r_[
|
||||
tile_data[
|
||||
(counter * 3):
|
||||
((counter+1) * 3)
|
||||
]
|
||||
]
|
||||
).flatten().tolist()
|
||||
)
|
||||
counter += 1
|
||||
yield (
|
||||
img_id,
|
||||
level,
|
||||
tilex,
|
||||
tiley,
|
||||
width_t,
|
||||
height_t,
|
||||
x0,
|
||||
y0,
|
||||
w,
|
||||
h,
|
||||
str(uuid.uuid4()),
|
||||
list(map(self.to_unsigned,tiledata))
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
|
||||
all_for_insertion = list(gen_tiles())
|
||||
print(f"Inserting {len(all_for_insertion)} tiles")
|
||||
|
||||
self.insert_tile_data(all_for_insertion)
|
||||
|
||||
|
||||
def process_and_insert_block_data(
|
||||
self,
|
||||
img_id: int,
|
||||
img_dims: Tuple[int, int],
|
||||
level: int,
|
||||
block_file: str,
|
||||
grid_size: int = 9
|
||||
) -> None:
|
||||
"""
|
||||
Process and insert block data for a specific level of an image.
|
||||
Args:
|
||||
img_id (int): ID of the image.
|
||||
img_dims (Tuple[int, int]): Dimensions of the image (width, height).
|
||||
level (int): Pyramid level.
|
||||
block_file (str): The memory-mapped block file.
|
||||
grid_size (int): Size of the grid.
|
||||
"""
|
||||
# Calculate the downsampled resolution for this level
|
||||
resolution = 2 ** level
|
||||
img_w, img_h = img_dims
|
||||
|
||||
# if level == 6:
|
||||
# row_size = 9
|
||||
# elif level == 7:
|
||||
# row_size = 5
|
||||
# else:
|
||||
|
||||
if level not in (6, 7):
|
||||
raise ValueError(f"Unsupported level ({level}). Level must be 6 or 7.")
|
||||
|
||||
# Load the memory-mapped block data
|
||||
block_data = self.load_memmapped_block_data(block_file)
|
||||
|
||||
# print(f"img dims: {img_dims}")
|
||||
# print(f"block data shape:{block_data.shape}")
|
||||
|
||||
tilew = img_w // resolution
|
||||
tileh = img_h // resolution
|
||||
width_t = img_w // tilew
|
||||
height_t = img_h // tileh
|
||||
|
||||
# print(f"tilew: {tilew}, tileh: {tileh}, width_t: {width_t}, height_t: {height_t}")
|
||||
|
||||
counter = 0
|
||||
# Loop through each block
|
||||
for tilex in range(tilew-2):
|
||||
for tiley in range(tileh-2):
|
||||
|
||||
x0 = tilex * width_t
|
||||
y0 = tiley * height_t
|
||||
w = grid_size
|
||||
h = 1
|
||||
|
||||
# print(f"{counter} - {level} - {tilex} - {tiley} - {tilew} - {tileh} - {width_t} - {height_t} - {x0} - {y0} - {w} - {h}")
|
||||
# Insert the block data into the database
|
||||
# print(f"{counter * grid_size * 3} - {(counter+1) * grid_size * 3}")
|
||||
|
||||
try:
|
||||
self.insert_block_data(
|
||||
img_id=img_id,
|
||||
level=level,
|
||||
tilex=tilex,
|
||||
tiley=tiley,
|
||||
x=w,
|
||||
y=h,
|
||||
x0=x0,
|
||||
y0=y0,
|
||||
w=w,
|
||||
h=h,
|
||||
block_data=list(
|
||||
(
|
||||
np.r_[
|
||||
block_data[
|
||||
(counter * grid_size * 3):
|
||||
((counter+1) * grid_size * 3)
|
||||
]
|
||||
# np.zeros((9 - grid_size) * 3)
|
||||
]
|
||||
).flatten().tolist()
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
pass
|
||||
counter += 1
|
||||
|
||||
def extract_image_base(self, fullpath: str) -> str:
|
||||
|
||||
term = r'.*(IMG_\d{4}|TESTIMG).*'
|
||||
m = re.match(term, fullpath)
|
||||
if m is not None:
|
||||
tkn = m.group(1)
|
||||
return f"img/{tkn}/{tkn}.jpeg", tkn
|
||||
raise ValueError(f"Could not extract image base from {fullpath}")
|
||||
|
||||
def process_image_analysis(
|
||||
self,
|
||||
img_path_in: str,
|
||||
max_level: int = 8
|
||||
) -> None:
|
||||
"""
|
||||
Process and insert all analysis data for an image across multiple levels.
|
||||
|
||||
Args:
|
||||
img_id (int): ID of the image.
|
||||
img_path (str): Full path.
|
||||
max_level (int): max exponent to pyr down splits
|
||||
"""
|
||||
img_path, img_tkn = self.extract_image_base(img_path_in)
|
||||
print(img_path)
|
||||
# assumes image has been inserted into image table
|
||||
img_id = self.get_image_id_by_img_name(os.path.basename(img_path))
|
||||
|
||||
print(f"IMAGE ID: {img_id}")
|
||||
|
||||
# insert image metadata
|
||||
if not os.path.exists(img_path):
|
||||
raise FileNotFoundError(f"{img_path} does not exist")
|
||||
|
||||
img = cv.imread(img_path)
|
||||
|
||||
width = img.shape[1]
|
||||
height = img.shape[0]
|
||||
|
||||
print(f"Processing image {img_path} {width}x{height} {img_path}")
|
||||
|
||||
for level in range(max_level):
|
||||
print(LEVELS_CONFIG[level])
|
||||
|
||||
if (LEVELS_CONFIG[level]["gridsplit"]) is not None:
|
||||
data_path = f"{os.path.dirname(img_path)}/{img_path}.histos.mmap"
|
||||
grid_size = LEVELS_CONFIG[level]["gridsplit"] + 1
|
||||
# Process histogram data for this level
|
||||
self.process_and_insert_histogram_data(
|
||||
img_id=img_id,
|
||||
img_dims=(width, height),
|
||||
level=level,
|
||||
histogram_file=data_path,
|
||||
grid_size=grid_size
|
||||
)
|
||||
if (LEVELS_CONFIG[level]["tiles"]):
|
||||
data_path = f"img/{img_tkn}/{img_tkn}.lab.pyr_{level}.jpeg.tiles.mmap"
|
||||
print(data_path)
|
||||
print("INSERTING TILE DATA")
|
||||
# Process tile data for this level
|
||||
self.process_and_insert_tile_data_batched(
|
||||
img_id=img_id,
|
||||
img_dims=(width, height),
|
||||
level=level,
|
||||
tile_file=data_path
|
||||
)
|
||||
|
||||
if (LEVELS_CONFIG[level]["targetblocks"]) is not None:
|
||||
data_path = f"{os.path.dirname(img_path)}/{img_path}.blocks.mmap"
|
||||
print("INSERTING BLOCK DATA")
|
||||
# Process block data for this level
|
||||
self.process_and_insert_block_data(
|
||||
img_id=img_id,
|
||||
img_dims=(width, height),
|
||||
level=level,
|
||||
block_file=data_path
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with DBConnection('testdb').get_connection() as db_conn:
|
||||
db_ops = DBOps(db_conn)
|
||||
db_ops.process_image_analysis('home/kfl/dev/git/avdb-py-v1/img/TESTIMG/TESTIMG.jpeg')
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import os
|
||||
import time
|
||||
from random import randint
|
||||
from ast import literal_eval
|
||||
import numpy as np
|
||||
import cv2 as cv
|
||||
import pymemcache
|
||||
|
||||
from avdb.db import DBConnection, DBOps
|
||||
|
||||
LIMIT=100
|
||||
LEVEL=randint(3,7)
|
||||
SEARCH_TILE_OFFSET=-2
|
||||
|
||||
class ImagePyramidState:
|
||||
def __init__(self, levels=8):
|
||||
self._connection = DBConnection('testdb')
|
||||
self._memcache = pymemcache.Client("127.0.0.1:11211")
|
||||
self.image_width = 1280
|
||||
self.image_height = 960
|
||||
self.levels = levels
|
||||
self.colorspace = 'lab'
|
||||
self.state_map = {}
|
||||
self.img_map = {}
|
||||
self.src_img_id, self.xlimit, self.ylimit, _ = self.select_initial_tile(src_img_id=None,level=LEVEL, inception=True)
|
||||
filename = self.get_imagefile_token_from_id(self.src_img_id)
|
||||
self.image_filetoken = os.path.basename(filename).removesuffix(".jpeg")
|
||||
print(self.image_filetoken)
|
||||
self.initialize_image_map()
|
||||
self.initialize_state_map()
|
||||
|
||||
|
||||
def initialize_state_map(self):
|
||||
"""
|
||||
Initialize state arrays with 0s (0 = original/no change)
|
||||
"""
|
||||
for lvl in range(1,8):
|
||||
self.state_map[lvl] = np.zeros(
|
||||
(
|
||||
# int(self.image_height/(2**lvl)),
|
||||
# int(self.image_width/(2**lvl)),
|
||||
int(self.xlimit),
|
||||
int(self.ylimit),
|
||||
3
|
||||
),
|
||||
dtype=np.uint16
|
||||
)
|
||||
|
||||
def initialize_image_map(self):
|
||||
"""
|
||||
Initialize image arrays with downsampled images
|
||||
"""
|
||||
for lvl in range(1,8):
|
||||
|
||||
imgpath = f"/home/kfl/dev/git/avdb-py-v1/img/{self.image_filetoken}/{self.image_filetoken}.{self.colorspace}.pyr_{lvl}_display.jpeg"
|
||||
|
||||
img = cv.imread(imgpath)
|
||||
print(f"imgpath:{imgpath} (level:) SHAPE: {img.shape}")
|
||||
self.img_map[lvl] = img
|
||||
|
||||
|
||||
def match_tile(self, src_img_id, src_tilex, src_tiley, search_tile, level=3, limit=100):
|
||||
"""
|
||||
pyrdata_id, img_id, level, tilex_a, tiley_a, x_a, y_a, x0_a, y0_a, w_a, h_a,
|
||||
tilex_b, tiley_b, x_b, y_b, x0_b, y0_b, w_b, h_b, hash, tile_data
|
||||
-- and tilex_a = {src_tilex}
|
||||
-- and tiley_a = {src_tiley}
|
||||
"""
|
||||
|
||||
search_query = f"""
|
||||
select *, (tile_data <-> '{search_tile}')
|
||||
from pyramiddata_2tiles
|
||||
where level = {level}
|
||||
and img_id <> {src_img_id}
|
||||
order by tile_data <-> '{search_tile}' asc
|
||||
limit {limit};
|
||||
"""
|
||||
psqldata = self._memcache.get(f"{str(search_tile).replace(', ', ',')}_{src_img_id}_{level}_{limit}")
|
||||
|
||||
if not psqldata:
|
||||
with self._connection.get_cursor() as cursor:
|
||||
|
||||
cursor.execute(search_query, (search_tile, level, src_img_id, LIMIT))
|
||||
rows = cursor.fetchall()
|
||||
self._memcache.set(f"{str(search_tile).replace(', ', ',')}_{src_img_id}_{level}_{limit}", rows)
|
||||
return rows
|
||||
|
||||
else:
|
||||
print ("Loaded data from memcached")
|
||||
return tuple(literal_eval(psqldata.decode("utf-8")))
|
||||
|
||||
def get_imagefile_token_from_id(self, src_img_id):
|
||||
"""
|
||||
"""
|
||||
if src_img_id is not None:
|
||||
search_q = f"""
|
||||
select *
|
||||
from avdb_image
|
||||
where img_id = {src_img_id};
|
||||
"""
|
||||
# print(search_q)
|
||||
with self._connection.get_cursor() as cursor:
|
||||
cursor.execute(search_q, src_img_id)
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
raise ValueError("No image found with the given img_id.")
|
||||
return row[2]
|
||||
|
||||
|
||||
def select_initial_tile(self, src_img_id=None, level=LEVEL, src_tilex=None, src_tiley=None, inception=False):
|
||||
"""
|
||||
"""
|
||||
if src_img_id is None:
|
||||
src_img_id = randint(1, 5) # 845)
|
||||
if src_tilex is None:
|
||||
src_tilex = randint(0, (1280 // (2**level)) - 5)
|
||||
if src_tiley is None:
|
||||
src_tiley = randint(0, (960 // (2**level)) - 5)
|
||||
|
||||
search_query = f"""
|
||||
select *
|
||||
from pyramiddata_2tiles
|
||||
where level = {level}
|
||||
and img_id = {src_img_id}
|
||||
and tilex_a = {src_tilex}
|
||||
and tiley_a = {src_tiley}
|
||||
limit {LIMIT};
|
||||
"""
|
||||
# print(search_query)
|
||||
with self._connection.get_cursor() as cursor:
|
||||
cursor.execute(search_query, (level, src_img_id, src_tilex, src_tiley))
|
||||
row = cursor.fetchone()
|
||||
if inception:
|
||||
return src_img_id, (1280 // (2**level)) - 1, (960 // (2**level)) - 1, row
|
||||
return src_img_id, src_tilex, src_tiley, row
|
||||
|
||||
|
||||
def run_generation(self):
|
||||
"""
|
||||
"""
|
||||
# choose random tile to update
|
||||
level = LEVEL
|
||||
generation_counter = 0
|
||||
|
||||
# grid = []
|
||||
# for y in range(0, self.ylimit - 6, 3):
|
||||
# for x in range(0, self.xlimit - 6, 3):
|
||||
# grid += [(y,x)]
|
||||
|
||||
# print(f"limits: {(self.ylimit, self.xlimit)}")
|
||||
|
||||
# for (y,x) in grid:
|
||||
while True:
|
||||
|
||||
lvl = level
|
||||
# _, tilex, tiley, init_tile = self.select_initial_tile(src_img_id=self.src_img_id, level=lvl, src_tilex=x, src_tiley=y, inception=False)
|
||||
_, x, y, init_tile = self.select_initial_tile(src_img_id=self.src_img_id, level=lvl, src_tilex=None, src_tiley=None, inception=False)
|
||||
|
||||
if init_tile is not None:
|
||||
|
||||
# print(f"INIT: {init_tile}")
|
||||
# find similar tiles to swap in
|
||||
found_tiles = self.match_tile(
|
||||
src_img_id=self.src_img_id,
|
||||
src_tilex=x,
|
||||
src_tiley=y,
|
||||
search_tile=init_tile[-1],
|
||||
level=lvl
|
||||
)
|
||||
# print(found_tiles[:3])
|
||||
|
||||
next_tile = found_tiles[randint(0, len(found_tiles)-1)]
|
||||
|
||||
# update img + state map
|
||||
sz = 2**lvl
|
||||
# print(next_tile)
|
||||
next_tile_ = list(map(int, next_tile[SEARCH_TILE_OFFSET].replace('[','').replace(']','').split(',')))
|
||||
|
||||
# print(f"next_tile_: {next_tile_}")
|
||||
# print(f"{next_tile[1]}, {init_tile[3]}, {init_tile[4]}")
|
||||
|
||||
self.img_map[lvl][y*sz:(y+1)*sz,x*sz:(x+1)*sz] = [
|
||||
next_tile_[0],
|
||||
next_tile_[1],
|
||||
next_tile_[2]
|
||||
]
|
||||
if generation_counter % 100 == 0:
|
||||
shape = self.state_map[lvl].shape
|
||||
print(shape)
|
||||
for x in range(shape[0]):
|
||||
for y in range(shape[1]):
|
||||
st = self.state_map[lvl][x,y]
|
||||
if st[0] > 0 or st[1] > 0.0 or st[2] > 0.0:
|
||||
print(
|
||||
f"x: {x}, y: {y} st: {st}"
|
||||
)
|
||||
|
||||
self.state_map[lvl][x, y] = [next_tile[1], next_tile[3], next_tile[4]]
|
||||
|
||||
cv.namedWindow("Image", cv.WINDOW_NORMAL)
|
||||
cv.resizeWindow("Image", 1280, 960)
|
||||
cv.imshow("Image", cv.cvtColor(self.img_map[lvl], cv.COLOR_Lab2RGB))
|
||||
time.sleep(0.2)
|
||||
cv.waitKey(1)
|
||||
generation_counter += 1
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ips = ImagePyramidState()
|
||||
for i in range(100):
|
||||
ips.run_generation()
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
psycopg2-binary>=2.9.6
|
||||
numpy>=1.21.0
|
||||
opencv-python>=4.5.5
|
||||
pytest>=7.0.0
|
||||
black>=22.0.0
|
||||
flake8>=4.0.0
|
||||
mypy>=0.900
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from avdb.db import DBOps, DBConnection
|
||||
|
||||
def process_single_file(db_ops, file_path):
|
||||
"""
|
||||
Process a single image file.
|
||||
|
||||
Args:
|
||||
db_ops (DBOps): An instance of DBOps for database operations.
|
||||
file_path (str): Path to the image file.
|
||||
"""
|
||||
print(f"Processing file: {file_path}")
|
||||
db_ops.process_image_analysis(file_path)
|
||||
|
||||
def process_directory(db_ops, directory_path):
|
||||
"""
|
||||
Process all image files in a directory.
|
||||
|
||||
Args:
|
||||
db_ops (DBOps): An instance of DBOps for database operations.
|
||||
directory_path (str): Path to the directory containing image files.
|
||||
"""
|
||||
print(f"Processing directory: {directory_path}")
|
||||
for root, _, files in os.walk(directory_path):
|
||||
# print(files)
|
||||
# print(root)
|
||||
for file in files:
|
||||
m = re.match(r'img_\d{4}\.jpeg', file.lower())
|
||||
if m is not None:
|
||||
file_path = os.path.join(root, file)
|
||||
process_single_file(db_ops, file_path)
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
parser = argparse.ArgumentParser(description="Run image analysis for a single file or all files in a directory.")
|
||||
parser.add_argument("path", help="Path to a single image file or a directory containing image files.")
|
||||
parser.add_argument("--dbname", default="testdb", help="Name of the database to use (default: testdb).")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Initialize database connection and operations
|
||||
with DBConnection(args.dbname).get_connection() as db_conn:
|
||||
db_ops = DBOps(db_conn)
|
||||
|
||||
# Determine if the path is a file or directory
|
||||
if os.path.isfile(args.path):
|
||||
process_single_file(db_ops, args.path)
|
||||
elif os.path.isdir(args.path):
|
||||
process_directory(db_ops, args.path)
|
||||
else:
|
||||
print(f"Error: Path '{args.path}' does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Test module for the ColorFeaturesPyramid class.
|
||||
|
||||
This module contains unit tests to verify the functionality of the ColorFeaturesPyramid class,
|
||||
including initialization, down-sampling, and tile analysis.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
|
||||
# from avdb.db import DBOps, DBConnection
|
||||
from avdb.color_features_pyramid import ColorFeaturesPyramid
|
||||
|
||||
|
||||
|
||||
def demo():
|
||||
"""
|
||||
Main function to demonstrate the usage of the ColorFeaturesPyramid class.
|
||||
|
||||
This function shows how to use the `_down_sample_and_analyze` method to perform
|
||||
down-sampling and tile analysis on an image.
|
||||
"""
|
||||
|
||||
for img_name in ["TESTIMG", "IMG_9997", "IMG_9998", "IMG_9999", "IMG_2327"]:
|
||||
|
||||
print(f"Processing image: {img_name}")
|
||||
|
||||
# Example image path and UUID
|
||||
img_path = f"img/{img_name}/{img_name}.jpeg"
|
||||
uuid = img_name
|
||||
|
||||
# Initialize the ColorFeaturesPyramid with custom parameters
|
||||
pyramid = ColorFeaturesPyramid(
|
||||
uuid=uuid,
|
||||
root_path=".",
|
||||
down_sampling=9,
|
||||
colorspace="lab"
|
||||
)
|
||||
|
||||
print("Starting down-sampling and analysis...")
|
||||
|
||||
# Run the down-sampling and analysis process
|
||||
results = list(pyramid._down_sample_and_analyze())
|
||||
|
||||
# Print the results for each level
|
||||
for level, (histograms, metadata) in results:
|
||||
imgw, imgh, tilesw, tilesh = metadata
|
||||
print(f"Level {level}:")
|
||||
print(f" Image dimensions: {imgw} x {imgh}")
|
||||
print(f" Tiles: {tilesw} x {tilesh}")
|
||||
print(f" Histograms shape: {histograms.shape}")
|
||||
print()
|
||||
|
||||
print("Down-sampling and analysis completed successfully.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
@@ -0,0 +1,123 @@
|
||||
"""
|
||||
Test module for the ColorFeaturesPyramid class.
|
||||
|
||||
This module contains unit tests to verify the functionality of the ColorFeaturesPyramid class,
|
||||
including initialization, down-sampling, and tile analysis.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import numpy as np
|
||||
from avdb.db import DBOps, DBConnection
|
||||
from avdb.color_features_pyramid import ColorFeaturesPyramid
|
||||
|
||||
def demo():
|
||||
"""
|
||||
Main function to demonstrate the usage of the ColorFeaturesPyramid class.
|
||||
|
||||
This function shows how to use the `_down_sample_and_analyze` method to perform
|
||||
down-sampling and tile analysis on an image.
|
||||
"""
|
||||
# Example image path and UUID
|
||||
img_path = "img/TESTIMG.jpeg"
|
||||
uuid = "TESTIMG"
|
||||
|
||||
# Initialize the ColorFeaturesPyramid with custom parameters
|
||||
pyramid = ColorFeaturesPyramid(
|
||||
uuid=uuid,
|
||||
root_path=".",
|
||||
down_sampling=9,
|
||||
colorspace='lab'
|
||||
)
|
||||
|
||||
print("Starting down-sampling and analysis...")
|
||||
|
||||
# Run the down-sampling and analysis process
|
||||
results = list(pyramid._down_sample_and_analyze())
|
||||
|
||||
# Print the results for each level
|
||||
for level, (histograms, metadata) in results:
|
||||
imgw, imgh, tilesw, tilesh = metadata
|
||||
print(f"Level {level}:")
|
||||
print(f" Image dimensions: {imgw} x {imgh}")
|
||||
print(f" Tiles: {tilesw} x {tilesh}")
|
||||
print(f" Histograms shape: {histograms.shape}")
|
||||
print()
|
||||
|
||||
print("Down-sampling and analysis completed successfully.")
|
||||
|
||||
def analyze_all_images(limit=10, dryrun=True):
|
||||
"""
|
||||
Iterate over all images in the 'img/' folder and analyze them using the ColorFeaturesPyramid class.
|
||||
|
||||
This function processes each image in the 'img/' folder, initializes the ColorFeaturesPyramid,
|
||||
and performs down-sampling and tile analysis for each image.
|
||||
"""
|
||||
with DBConnection('testdb').get_connection() as db_conn:
|
||||
db_ops = DBOps(db_conn)
|
||||
|
||||
img_folder = "img"
|
||||
if not os.path.exists(img_folder):
|
||||
print(f"Error: The folder '{img_folder}' does not exist.")
|
||||
return
|
||||
|
||||
# Get all image files in the folder
|
||||
image_extensions = ('.jpg', '.jpeg')
|
||||
image_files = [
|
||||
f"{f}/{f}.jpeg" for f in os.listdir(img_folder)
|
||||
]
|
||||
|
||||
if not image_files:
|
||||
print(f"No images found in the folder '{img_folder}'.")
|
||||
return
|
||||
|
||||
print(f"Found {len(image_files)} images in the folder '{img_folder}'.")
|
||||
|
||||
for img_file in image_files[:limit]:
|
||||
img_path = os.path.join(img_folder, img_file)
|
||||
uuid = os.path.splitext(img_file.split('/')[1])[0] # Use the filename (without extension) as UUID
|
||||
|
||||
data_file_exists = os.path.exists(img_path.replace(".jpeg", ".lab.pyr_3.jpeg.tiles.mmap"))
|
||||
print(img_path.replace(".jpeg", ".lab.pyr_3.jpeg.tiles.mmap"))
|
||||
print(data_file_exists)
|
||||
# Check if the analysis files exist using level 7 as proxy
|
||||
if not data_file_exists:
|
||||
# img = cv.imread(img_path)
|
||||
# print(f"\nAnalyzing image: {img_path} - {os.path.basename(img_path)} - {len(image_files)} - {img.shape}")
|
||||
|
||||
# if not dryrun:
|
||||
# img_name = os.path.basename(img_path)
|
||||
# img_id_out = db_ops.insert_image(
|
||||
# img_path,
|
||||
# img_name,
|
||||
# img.shape[0],
|
||||
# img.shape[1]
|
||||
# )
|
||||
# print(f"Inserted image {i+1} {img_id_out}")
|
||||
|
||||
# Initialize the ColorFeaturesPyramid with custom parameters
|
||||
pyramid = ColorFeaturesPyramid(
|
||||
uuid=uuid,
|
||||
root_path=".",
|
||||
down_sampling=9,
|
||||
colorspace='lab'
|
||||
)
|
||||
|
||||
# Run the down-sampling and analysis process
|
||||
results = list(pyramid._down_sample_and_analyze())
|
||||
|
||||
# Print the results for each level
|
||||
for level, (histograms, metadata) in results:
|
||||
imgw, imgh, tilesw, tilesh = metadata
|
||||
print(f" Level {level}:")
|
||||
print(f" Image dimensions: {imgw} x {imgh}")
|
||||
print(f" Tiles: {tilesw} x {tilesh}")
|
||||
print(f" Histograms shape: {histograms.shape}\n\n")
|
||||
else:
|
||||
print(f"\nSkipping analysis file already exists:\n\t{img_file}\n")
|
||||
|
||||
print("\nAnalysis of all images completed successfully.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
# demo()
|
||||
analyze_all_images(limit=999, dryrun=False)
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Directory containing the JPEG images
|
||||
IMG_DIR="img"
|
||||
|
||||
# Check if the directory exists
|
||||
if [ ! -d "$IMG_DIR" ]; then
|
||||
echo "Directory $IMG_DIR does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Loop through each JPEG file in the directory
|
||||
for img_file in "$IMG_DIR"/*.jpg "$IMG_DIR"/*.jpeg; do
|
||||
# Check if the file exists (to handle cases where no files match the pattern)
|
||||
if [ -f "$img_file" ]; then
|
||||
# Extract the base name of the file (without extension)
|
||||
base_name=$(basename "$img_file" | cut -d. -f1)
|
||||
|
||||
# Create a folder with the same base name
|
||||
mkdir -p "$IMG_DIR/$base_name"
|
||||
|
||||
# Move the image file into the folder
|
||||
mv "$img_file" "$IMG_DIR/$base_name/"
|
||||
|
||||
echo "Moved $img_file to $IMG_DIR/$base_name/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done!"
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Script to iterate over histogram data and insert it into the database.
|
||||
This is a stub for future development.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import numpy as np
|
||||
import sqlalchemy
|
||||
from sqlalchemy import text, exc
|
||||
|
||||
# Database URL from alembic.ini
|
||||
DATABASE_URL = "postgresql+psycopg2://postgres:pgRocks@localhost:5432/testdb"
|
||||
BASE_DIR = '/home/kfl/dev/git/avdb-py-v1'
|
||||
|
||||
def insert_histogram_data():
|
||||
"""
|
||||
Iterate over histogram data for levels 3-5 and insert it into the database.
|
||||
This is a stub and will be developed over several prompts.
|
||||
"""
|
||||
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||
|
||||
# Placeholder for histogram data directory
|
||||
img_folder = "img"
|
||||
inserted_count = 0
|
||||
skipped_count = 0
|
||||
duplicate_count = 0
|
||||
|
||||
# Iterate over histogram data files (stub)
|
||||
for root, dirs, files in os.walk(img_folder):
|
||||
for file in files:
|
||||
if file.lower().endswith('.mmap'):
|
||||
histogram_file_path = os.path.join(BASE_DIR, root, file)
|
||||
print(histogram_file_path)
|
||||
level = 0
|
||||
m = re.match(r'.*(\d{1})\.jpeg.histos.mmap', histogram_file_path)
|
||||
if m:
|
||||
level = int(m.group(1))
|
||||
if level in (3,4,5):
|
||||
# Extract histogram metadata (stub)
|
||||
try:
|
||||
histogram_values = [] # Placeholder
|
||||
|
||||
gridsplit = 0
|
||||
if level == 3:
|
||||
gridsplit = 8
|
||||
elif level == 4:
|
||||
gridsplit = 4
|
||||
elif level == 5:
|
||||
gridsplit = 2
|
||||
|
||||
data = np.memmap(
|
||||
histogram_file_path,
|
||||
dtype="float32",
|
||||
mode='r+',
|
||||
shape=((gridsplit+1)**2,48)
|
||||
)
|
||||
print(data.shape)
|
||||
for row in data:
|
||||
print(row)
|
||||
|
||||
# # Insert into the database (stub)
|
||||
# with engine.connect() as connection:
|
||||
# query = text(
|
||||
# """
|
||||
# INSERT INTO histogram_table (level, histogram_values)
|
||||
# VALUES (:level, :histogram_values)
|
||||
# """
|
||||
# )
|
||||
# connection.execute(
|
||||
# query,
|
||||
# {"level": level, "histogram_values": histogram_values}
|
||||
# )
|
||||
# connection.commit()
|
||||
# inserted_count += 1
|
||||
# print(f"Inserted histogram data for level {level}")
|
||||
except exc.IntegrityError as e:
|
||||
# Handle duplicate key violations
|
||||
print(f"Duplicate histogram entry skipped for {histogram_file_path}: {e}")
|
||||
duplicate_count += 1
|
||||
except Exception as e:
|
||||
print(f"Failed to insert histogram data for {histogram_file_path}: {e}")
|
||||
skipped_count += 1
|
||||
|
||||
print(f"\nTotal histogram entries inserted: {inserted_count}")
|
||||
print(f"Total duplicates skipped: {duplicate_count}")
|
||||
print(f"Total entries skipped due to errors: {skipped_count}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
insert_histogram_data()
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Script to iterate over all images in the img folder and insert their metadata into the avdb_image table.
|
||||
"""
|
||||
import os
|
||||
from PIL import Image
|
||||
import sqlalchemy
|
||||
from sqlalchemy import text, exc
|
||||
|
||||
# Database URL from alembic.ini
|
||||
DATABASE_URL = "postgresql+psycopg2://postgres:pgRocks@localhost:5432/testdb"
|
||||
|
||||
def get_image_metadata(image_path):
|
||||
"""Extract metadata (width, height) from an image."""
|
||||
with Image.open(image_path) as img:
|
||||
width, height = img.size
|
||||
return width, height
|
||||
|
||||
def insert_images_into_db():
|
||||
"""Iterate over all images in the img folder and insert their metadata into the avdb_image table."""
|
||||
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||
|
||||
# Iterate over all subdirectories in the img folder
|
||||
img_folder = "img"
|
||||
inserted_count = 0
|
||||
skipped_count = 0
|
||||
test_shim = 'TESTIMG.jpeg'
|
||||
duplicate_count = 0
|
||||
|
||||
for root, dirs, files in os.walk(img_folder):
|
||||
for file in files:
|
||||
# test nmode
|
||||
if file.endswith(test_shim):
|
||||
# if file.lower().endswith(('.jpg', '.jpeg')):
|
||||
image_path = os.path.join(root, file)
|
||||
img_name = file
|
||||
|
||||
# Extract width and height
|
||||
try:
|
||||
width, height = get_image_metadata(image_path)
|
||||
except Exception as e:
|
||||
print(f"Failed to extract metadata for {image_path}: {e}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Insert into the database
|
||||
try:
|
||||
with engine.connect() as connection:
|
||||
query = text(
|
||||
"""
|
||||
INSERT INTO avdb_image (img_path, img_name, width, height)
|
||||
VALUES (:img_path, :img_name, :width, :height)
|
||||
"""
|
||||
)
|
||||
connection.execute(
|
||||
query,
|
||||
{"img_path": image_path, "img_name": img_name, "width": width, "height": height}
|
||||
)
|
||||
connection.commit()
|
||||
inserted_count += 1
|
||||
print(f"Inserted: {img_name} (Width: {width}, Height: {height})")
|
||||
except exc.IntegrityError:
|
||||
# Handle duplicate key violations
|
||||
print(f"Duplicate entry skipped for {image_path}")
|
||||
duplicate_count += 1
|
||||
except Exception as e:
|
||||
print(f"Failed to insert {image_path} into the database: {e}")
|
||||
skipped_count += 1
|
||||
|
||||
print(f"\nTotal images inserted: {inserted_count}")
|
||||
print(f"Total duplicates skipped: {duplicate_count}")
|
||||
print(f"Total images skipped due to errors: {skipped_count}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
insert_images_into_db()
|
||||
@@ -0,0 +1,45 @@
|
||||
insert into pyramiddata_2tiles (
|
||||
pyrdata_id, img_id, level, tilex_a, tiley_a, x_a, y_a, x0_a, y0_a, w_a, h_a,
|
||||
tilex_b, tiley_b, x_b, y_b, x0_b, y0_b, w_b, h_b, hash, tile_data
|
||||
)
|
||||
select a.pyrdata_id as pyrdata_id
|
||||
,a.img_id as img_id
|
||||
,a.level as level
|
||||
,a.tilex as tilex_a
|
||||
,a.tiley as tiley_a
|
||||
,a.x as x_a
|
||||
,a.y as y_a
|
||||
,a.x0 as x0_a
|
||||
,a.y0 as y0_a
|
||||
,a.w as w_a
|
||||
,a.h as h_a
|
||||
,b.tilex as tilex_b
|
||||
,b.tiley as tilex_b
|
||||
,a.x as x_b
|
||||
,a.y as y_b
|
||||
,b.x0 as x0_b
|
||||
,b.y0 as y0_b
|
||||
,b.w as w_b
|
||||
,b.h as h_b
|
||||
,a.hash as hash
|
||||
,a.tile_data||b.tile_data as tile_data
|
||||
from (
|
||||
select *
|
||||
from pyramiddata
|
||||
where level >= 3
|
||||
and level <= 6
|
||||
--and img_id = 1
|
||||
and tile_data is not null
|
||||
) a
|
||||
JOIN (
|
||||
select *
|
||||
from pyramiddata
|
||||
where level >= 4
|
||||
and level <= 7
|
||||
--and img_id = 1
|
||||
and tile_data is not null
|
||||
) b
|
||||
on a.img_id = b.img_id
|
||||
and a.level + 1 = b.level
|
||||
and a.tilex / 2 = b.tilex
|
||||
and a.tiley / 2 = b.tiley
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the img directory
|
||||
cd img || exit
|
||||
|
||||
# Loop through each folder in the img directory
|
||||
for folder in IMG_*; do
|
||||
# Check if the folder exists and is a directory
|
||||
if [ -d "$folder" ]; then
|
||||
# Navigate into the folder
|
||||
cd "$folder" || continue
|
||||
|
||||
for file in *; do
|
||||
if [[ -f "$file" ]] && ! [[ "$file" =~ ^IMG_[0-9]{4}\.jpeg$ ]]; then
|
||||
rm "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
# Navigate back to the img directory
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,33 @@
|
||||
# def iter_and_append_from_csv(csvfile):
|
||||
# """
|
||||
# Read and iterate over a CSV file.
|
||||
# Args:
|
||||
# csvfile (file): A file object representing the CSV file to read.
|
||||
|
||||
# Yields:
|
||||
# list: A row from the CSV file as a list of strings.
|
||||
# """
|
||||
# csvreader = csv.reader(csvfile)
|
||||
# for row in csvreader:
|
||||
# yield row
|
||||
|
||||
# def _read_csv_data(filename: str, row_indices: List[int], col_indices: List[int]) -> List[List[str]]:
|
||||
# """
|
||||
# Helper function to read and extract specific rows and columns from a CSV file.
|
||||
|
||||
# Args:
|
||||
# filename (str): Path to the CSV file.
|
||||
# row_indices (List[int]): List of row indices to extract.
|
||||
# col_indices (List[int]): List of column indices to extract.
|
||||
|
||||
# Returns:
|
||||
# List[List[str]]: A list of rows, where each row is a list of values at the specified column indices.
|
||||
# """
|
||||
# try:
|
||||
# with open(filename, 'r', encoding='utf-8') as csvfile:
|
||||
# csvfile_data = list(iter_and_append_from_csv(csvfile))
|
||||
# return [[csvfile_data[row][col] for col in col_indices] for row in row_indices]
|
||||
# except FileNotFoundError as e:
|
||||
# print(e)
|
||||
# return []
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to test the process_image_analysis function in db.py
|
||||
# This script will run the function and stop on errors.
|
||||
|
||||
# Define the path to the Python script and the image to process
|
||||
# PYTHON_SCRIPT="src/db.py"
|
||||
# Replace with the actual image path
|
||||
IMAGE_PATH="/home/kfl/dev/git/avdb-py-v1/img/TESTIMG/TESTIMG.jpeg"
|
||||
|
||||
# Function to run the test
|
||||
test_process_image_analysis() {
|
||||
echo "Starting test for process_image_analysis..."
|
||||
|
||||
# Run the Python script and capture the output
|
||||
output=$(python3 -c "
|
||||
import sys
|
||||
sys.path.append('src')
|
||||
from db import DBOps, DBConnection
|
||||
|
||||
# Initialize the database connection
|
||||
db_conn = DBConnection('testdb')
|
||||
db_ops = DBOps(db_conn)
|
||||
|
||||
# Run the process_image_analysis function
|
||||
try:
|
||||
# raise(ValueError('testerr'))
|
||||
db_ops.process_image_analysis('$IMAGE_PATH')
|
||||
print('process_image_analysis completed successfully.')
|
||||
except Exception as e:
|
||||
print(f'Error in process_image_analysis: {e}')
|
||||
sys.exit(1)
|
||||
" 2>&1 | tee error_log.txt
|
||||
)
|
||||
# Check the output for errors
|
||||
if [[ "$output" == *"Error"* ]]; then
|
||||
echo "Test failed: $output"
|
||||
exit 1
|
||||
else
|
||||
echo "Test passed: $output"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run the test
|
||||
test_process_image_analysis
|
||||
|
||||
# Exit with the appropriate status
|
||||
# exit $?
|
||||
@@ -0,0 +1,51 @@
|
||||
# Test Plan for `ColorFeaturesPyramid`
|
||||
|
||||
## Overview
|
||||
This document outlines the plan for testing and documenting the `ColorFeaturesPyramid` class.
|
||||
|
||||
## 1. Understanding the `ColorFeaturesPyramid` Class
|
||||
- The `ColorFeaturesPyramid` class performs pyramid down-sampling and color feature analysis on images.
|
||||
- It relies on the `ColorFeaturesTile` class for tile-based analysis.
|
||||
- Key parameters:
|
||||
- `uuid`: Identifier for the image.
|
||||
- `tilew` and `tileh`: Dimensions of the tiles for analysis.
|
||||
- `down_sampling`: Number of down-sampling levels.
|
||||
- `colorspace`: Color space for analysis (e.g., `lab`, `rgb`, `hsv`).
|
||||
- The class reads an image, down-samples it, and analyzes the tiles at each level.
|
||||
|
||||
## 2. Design the Test Function
|
||||
### Test Scope
|
||||
Unit tests to verify the functionality of the `ColorFeaturesPyramid` class.
|
||||
|
||||
### Test Cases
|
||||
1. **Initialization Test**:
|
||||
- Verify that the class initializes correctly with default and custom parameters.
|
||||
- Ensure that the `check_params` function from `utils.py` is used correctly.
|
||||
2. **Down-Sampling Test**:
|
||||
- Verify that the down-sampling process works as expected.
|
||||
- Check that the down-sampled images are saved correctly.
|
||||
3. **Tile Analysis Test**:
|
||||
- Verify that the tile analysis produces the expected output.
|
||||
- Ensure that the histograms are computed correctly.
|
||||
4. **Edge Cases**:
|
||||
- Test with invalid inputs (e.g., non-existent image, invalid color space).
|
||||
- Test with extreme values for parameters (e.g., very large or small tile sizes).
|
||||
|
||||
### Testing Framework
|
||||
Use `pytest` for writing and running the tests.
|
||||
|
||||
## 3. Document the Test Function
|
||||
- Write clear docstrings for the test function.
|
||||
- Include examples of how to use the `ColorFeaturesPyramid` class.
|
||||
- Document the expected behavior and assumptions.
|
||||
- Add comments to explain the purpose of each test case.
|
||||
|
||||
## 4. Plan the Implementation
|
||||
- Create a new test file: `test_color_features_pyramid.py`.
|
||||
- Implement the test function and test cases.
|
||||
- Ensure the test function is modular and reusable.
|
||||
- Use the `check_params` function from `utils.py` to validate parameters.
|
||||
|
||||
## 5. Verify the Plan
|
||||
- Review the plan to ensure all aspects are covered.
|
||||
- Confirm that the test function aligns with the purpose of the `ColorFeaturesPyramid` class.
|
||||
@@ -0,0 +1,3 @@
|
||||
# tests/__init__.py
|
||||
|
||||
# This file can be empty or include test-specific initialization
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 701 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,8 @@
|
||||
import unittest
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Discover and run all tests in the tests directory
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.discover('tests', pattern='test_*.py')
|
||||
runner = unittest.TextTestRunner()
|
||||
runner.run(suite)
|
||||
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Test script to verify the Alembic migration for the avdb_image table.
|
||||
"""
|
||||
import sqlalchemy
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
# Database URL from alembic.ini
|
||||
DATABASE_URL = "postgresql+psycopg2://postgres:pgRocks@localhost:5432/testdb"
|
||||
|
||||
def test_avdb_image_table_exists():
|
||||
"""Test if the avdb_image table exists in the database."""
|
||||
engine = sqlalchemy.create_engine(DATABASE_URL)
|
||||
inspector = inspect(engine)
|
||||
|
||||
# Print database and table configuration
|
||||
print("Database URL:", DATABASE_URL)
|
||||
print("\nChecking for avdb_image table...")
|
||||
|
||||
# Check if the table exists
|
||||
assert inspector.has_table("avdb_image"), "avdb_image table does not exist!"
|
||||
print("✓ avdb_image table exists.")
|
||||
|
||||
# Verify the table structure
|
||||
columns = inspector.get_columns("avdb_image")
|
||||
column_names = [column["name"] for column in columns]
|
||||
|
||||
print("\nColumns in avdb_image table:")
|
||||
for column in columns:
|
||||
print(f" - {column['name']}: {column['type']}, Nullable: {column['nullable']}")
|
||||
|
||||
expected_columns = ["img_id", "img_path", "img_name", "width", "height", "created_at"]
|
||||
for col in expected_columns:
|
||||
assert col in column_names, f"Column '{col}' not found in avdb_image table!"
|
||||
print("\n✓ All expected columns are present in the avdb_image table.")
|
||||
|
||||
# Verify primary key
|
||||
primary_key = inspector.get_pk_constraint("avdb_image")
|
||||
assert primary_key is not None, "Primary key not found in avdb_image table!"
|
||||
assert primary_key["constrained_columns"] == ["img_id"], "Primary key is not set on img_id!"
|
||||
print("✓ Primary key is correctly set on img_id.")
|
||||
|
||||
# Verify nullable constraints
|
||||
nullable_columns = [col["name"] for col in columns if col["nullable"]]
|
||||
non_nullable_columns = ["img_path", "img_name", "width", "height"]
|
||||
for col in non_nullable_columns:
|
||||
assert col not in nullable_columns, f"Column '{col}' should not be nullable!"
|
||||
print("✓ Non-nullable constraints are correctly set.")
|
||||
|
||||
print("\nAll tests passed! The avdb_image table was created successfully.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_avdb_image_table_exists()
|
||||
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Test module for the ColorFeaturesPyramid class.
|
||||
|
||||
This module contains unit tests to verify the functionality of the ColorFeaturesPyramid class,
|
||||
including initialization, down-sampling, and tile analysis.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
|
||||
from color_features_pyramid import ColorFeaturesPyramid
|
||||
|
||||
|
||||
class TestColorFeaturesPyramid:
|
||||
"""
|
||||
Test suite for the ColorFeaturesPyramid class.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def setup_image(self, tmp_path):
|
||||
"""
|
||||
Fixture to set up a test image for the ColorFeaturesPyramid tests.
|
||||
|
||||
Args:
|
||||
tmp_path: Pytest fixture providing a temporary directory.
|
||||
|
||||
Returns:
|
||||
tuple: Path to the test image and the image UUID.
|
||||
"""
|
||||
# Create a temporary directory for the test image
|
||||
img_dir = "/home/kfl/dev/git/avdb-py-v1/img"
|
||||
|
||||
# Create a simple test image
|
||||
img = np.zeros((100, 100, 3), dtype=np.uint16)
|
||||
img[:, :] = (255, 0, 0) # Red image
|
||||
|
||||
img_path = img_dir + "/TESTIMG.jpeg"
|
||||
# cv.imwrite(str(img_path), img)
|
||||
|
||||
return str(img_path), "TESTIMG"
|
||||
|
||||
def test_initialization_default_params(self, setup_image):
|
||||
"""
|
||||
Test that the ColorFeaturesPyramid class initializes correctly with default parameters.
|
||||
|
||||
Args:
|
||||
setup_image: Fixture providing the test image path and UUID.
|
||||
"""
|
||||
img_path, uuid = setup_image
|
||||
|
||||
# Initialize with default parameters
|
||||
pyramid = ColorFeaturesPyramid(uuid=uuid, root_path=os.path.dirname(img_path))
|
||||
|
||||
# Verify default parameters
|
||||
assert pyramid.uuid == uuid
|
||||
assert pyramid.down_sampling_levels == 9
|
||||
assert pyramid.colorspace == hsv
|
||||
|
||||
def test_initialization_custom_params(self, setup_image):
|
||||
"""
|
||||
Test that the ColorFeaturesPyramid class initializes correctly with custom parameters.
|
||||
|
||||
Args:
|
||||
setup_image: Fixture providing the test image path and UUID.
|
||||
"""
|
||||
img_path, uuid = setup_image
|
||||
|
||||
# Initialize with custom parameters
|
||||
pyramid = ColorFeaturesPyramid(
|
||||
uuid=uuid,
|
||||
root_path=os.path.dirname(img_path),
|
||||
down_sampling=5,
|
||||
colorspace="rgb"
|
||||
)
|
||||
|
||||
# Verify custom parameters
|
||||
assert pyramid.uuid == uuid
|
||||
assert pyramid.down_sampling_levels == 5
|
||||
assert pyramid.colorspace == "rgb"
|
||||
|
||||
def test_down_sampling(self, setup_image):
|
||||
"""
|
||||
Test that the down-sampling process works as expected.
|
||||
|
||||
Args:
|
||||
setup_image: Fixture providing the test image path and UUID.
|
||||
"""
|
||||
img_path, uuid = setup_image
|
||||
|
||||
# Initialize the pyramid
|
||||
pyramid = ColorFeaturesPyramid(uuid=uuid, root_path=os.path.dirname(img_path))
|
||||
|
||||
# Run the down-sampling and analysis process
|
||||
results = list(pyramid._down_sample_and_analyze())
|
||||
|
||||
# Verify that results are generated for each level
|
||||
assert len(results) > 0
|
||||
|
||||
# Verify that down-sampled images are saved
|
||||
for level in range(1, pyramid.down_sampling_levels):
|
||||
# Construct the correct path for the down-sampled image
|
||||
ds_img_filename = f"{uuid}/{uuid}.{pyramid.colorspace}.pyr_{level}.jpeg"
|
||||
ds_img_path = os.path.join(os.path.dirname(img_path), ds_img_filename)
|
||||
assert os.path.exists(ds_img_path), f"Down-sampled image not found: {ds_img_path}"
|
||||
|
||||
def test_tile_analysis(self, setup_image):
|
||||
"""
|
||||
Test that the tile analysis produces the expected output.
|
||||
|
||||
Args:
|
||||
setup_image: Fixture providing the test image path and UUID.
|
||||
"""
|
||||
img_path, uuid = setup_image
|
||||
|
||||
# Initialize the pyramid
|
||||
pyramid = ColorFeaturesPyramid(uuid=uuid, root_path=os.path.dirname(img_path))
|
||||
|
||||
# Run the down-sampling and analysis process
|
||||
results = list(pyramid._down_sample_and_analyze())
|
||||
|
||||
# Verify that results contain histograms and metadata
|
||||
for level, (histograms, metadata) in results:
|
||||
assert histograms is not None
|
||||
assert len(metadata) == 4 # (imgw, imgh, tilesw, tilesh)
|
||||
|
||||
def test_edge_case_invalid_image(self):
|
||||
"""
|
||||
Test that the class handles invalid image paths gracefully.
|
||||
"""
|
||||
# Initialize with a non-existent image
|
||||
with pytest.raises(Exception):
|
||||
pyramid = ColorFeaturesPyramid(uuid="nonexistent_image", root_path="/tmp")
|
||||
list(pyramid._down_sample_and_analyze())
|
||||
|
||||
def test_edge_case_invalid_colorspace(self, setup_image):
|
||||
"""
|
||||
Test that the class handles invalid color spaces gracefully.
|
||||
|
||||
Args:
|
||||
setup_image: Fixture providing the test image path and UUID.
|
||||
"""
|
||||
img_path, uuid = setup_image
|
||||
|
||||
# Initialize with an invalid color space
|
||||
with pytest.raises(Exception):
|
||||
pyramid = ColorFeaturesPyramid(uuid=uuid, root_path=os.path.dirname(img_path), colorspace="invalid")
|
||||
list(pyramid._down_sample_and_analyze())
|
||||
@@ -0,0 +1,131 @@
|
||||
import unittest
|
||||
import os
|
||||
import numpy as np
|
||||
import csv
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, patch
|
||||
import psycopg2
|
||||
from psycopg2 import extras
|
||||
|
||||
from avdb.db import DBOps
|
||||
from avdb.analysis import convert_mmap_to_csv
|
||||
|
||||
class TestDBOps(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Mock the database connection and cursor
|
||||
self.mock_conn = MagicMock()
|
||||
self.mock_cursor = MagicMock()
|
||||
self.mock_conn.get_cursor.return_value = self.mock_cursor
|
||||
self.mock_conn.get_connection.return_value = self.mock_conn
|
||||
|
||||
# Initialize DBOps with the mocked connection
|
||||
self.db_ops = DBOps(self.mock_conn)
|
||||
|
||||
def tearDown(self):
|
||||
# Clean up any temporary files created during tests
|
||||
if os.path.exists("test_histogram.mmap"):
|
||||
os.remove("test_histogram.mmap")
|
||||
if os.path.exists("test_histogram.csv"):
|
||||
os.remove("test_histogram.csv")
|
||||
if os.path.exists("test_tile.mmap"):
|
||||
os.remove("test_tile.mmap")
|
||||
|
||||
def test_convert_mmap_to_csv(self):
|
||||
# Create a temporary .mmap file for testing
|
||||
mmap_file_path = "test_histogram.mmap"
|
||||
csv_file_path = "test_histogram.csv"
|
||||
|
||||
# Create dummy data for the .mmap file
|
||||
dummy_data = np.random.rand(81, 48).astype('float32')
|
||||
with open(mmap_file_path, 'wb') as f:
|
||||
f.write(dummy_data.tobytes())
|
||||
|
||||
# Call the conversion function
|
||||
convert_mmap_to_csv(mmap_file_path, csv_file_path)
|
||||
|
||||
# Verify the CSV file was created and contains the correct data
|
||||
with open(csv_file_path, 'r') as csv_file:
|
||||
reader = csv.reader(csv_file)
|
||||
csv_data = list(reader)
|
||||
|
||||
# Convert the CSV data back to a numpy array for comparison
|
||||
csv_data_array = np.array(csv_data, dtype='float32')
|
||||
|
||||
# Assert that the data matches
|
||||
np.testing.assert_array_equal(csv_data_array, dummy_data)
|
||||
|
||||
def test_insert_tile_data(self):
|
||||
# Create dummy tile data
|
||||
dummy_rows = [
|
||||
(
|
||||
1, # img_id
|
||||
3, # level
|
||||
0, # tilex
|
||||
0, # tiley
|
||||
100, # x
|
||||
100, # y
|
||||
50, # x0
|
||||
50, # y0
|
||||
10, # w
|
||||
10, # h
|
||||
str(uuid.uuid4()), # hash
|
||||
[1, 2, 3] # tile_data
|
||||
),
|
||||
(
|
||||
1, # img_id
|
||||
3, # level
|
||||
1, # tilex
|
||||
1, # tiley
|
||||
200, # x
|
||||
200, # y
|
||||
100, # x0
|
||||
100, # y0
|
||||
10, # w
|
||||
10, # h
|
||||
str(uuid.uuid4()), # hash
|
||||
[4, 5, 6] # tile_data
|
||||
)
|
||||
]
|
||||
|
||||
# Mock the execute_batch method
|
||||
with patch('avdb.db.psycopg2.extras.execute_batch') as mock_execute_batch:
|
||||
# Call the insert_tile_data method
|
||||
self.db_ops.insert_tile_data(dummy_rows)
|
||||
|
||||
# Verify that execute_batch was called with the correct query and data
|
||||
mock_execute_batch.assert_called_once_with(
|
||||
self.mock_cursor,
|
||||
"""
|
||||
INSERT INTO pyramiddata_level3 (
|
||||
img_id, level, tilex, tiley, x, y, x0, y0, w, h, hash, tile_data
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
dummy_rows
|
||||
)
|
||||
|
||||
def test_process_and_insert_tile_data_batched(self):
|
||||
# Create a temporary .mmap file for testing
|
||||
mmap_file_path = "test_tile.mmap"
|
||||
dummy_data = np.random.randint(0, 255, size=(100, 3), dtype=np.uint16)
|
||||
with open(mmap_file_path, 'wb') as f:
|
||||
f.write(dummy_data.tobytes())
|
||||
|
||||
# Mock the load_memmapped_tile_data method to return the dummy data
|
||||
self.db_ops.load_memmapped_tile_data = MagicMock(return_value=dummy_data)
|
||||
|
||||
# Mock the execute_batch method
|
||||
with patch('avdb.db.psycopg2.extras.execute_batch') as mock_execute_batch:
|
||||
# Call the process_and_insert_tile_data_batched method
|
||||
self.db_ops.process_and_insert_tile_data_batched(
|
||||
img_id=1,
|
||||
img_dims=(1000, 1000),
|
||||
level=3,
|
||||
tile_file=mmap_file_path
|
||||
)
|
||||
|
||||
# Verify that execute_batch was called
|
||||
mock_execute_batch.assert_called()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user