initial commit of new version

This commit is contained in:
tms
2026-10-04 18:24:18 -04:00
parent 994fac96e0
commit 57220375a3
43 changed files with 3429 additions and 1 deletions
+8
View File
@@ -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']
+187
View File
@@ -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()
+23
View File
@@ -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
+10
View File
@@ -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 @@
+9
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
+230
View File
@@ -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)
)
+261
View File
@@ -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)
+72
View File
@@ -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
View File
@@ -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')
+213
View File
@@ -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()