Skip to content

Commit

Permalink
Adds HueHistogram feature adapted from Cineast.
Browse files Browse the repository at this point in the history
Signed-off-by: Ralph Gasser <[email protected]>
  • Loading branch information
ppanopticon committed Aug 21, 2024
1 parent d1c29c3 commit 2edf650
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package org.vitrivr.engine.core.features.averagecolor

import org.vitrivr.engine.core.context.IndexContext
import org.vitrivr.engine.core.context.QueryContext
import org.vitrivr.engine.core.model.color.MutableRGBFloatColorContainer
import org.vitrivr.engine.core.model.color.RGBByteColorContainer
import org.vitrivr.engine.core.model.color.RGBFloatColorContainer
import org.vitrivr.engine.core.model.color.rgb.MutableRGBFloatColorContainer
import org.vitrivr.engine.core.model.color.rgb.RGBByteColorContainer
import org.vitrivr.engine.core.model.color.rgb.RGBFloatColorContainer
import org.vitrivr.engine.core.model.content.Content
import org.vitrivr.engine.core.model.content.element.ImageContent
import org.vitrivr.engine.core.model.descriptor.vector.FloatVectorDescriptor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.vitrivr.engine.core.model.color

import org.vitrivr.engine.core.model.color.hsv.HSVColorContainer
import org.vitrivr.engine.core.model.color.rgb.RGBByteColorContainer
import org.vitrivr.engine.core.model.color.rgb.RGBFloatColorContainer

/**
* A color converter that provides methods to convert between different color spaces.
*
* Adapted from Cineast.
*
* @author Ralph Gasser
* @version 1.0.0
*/
object ColorConverter {

/**
* Converts a [RGBByteColorContainer] to a [HSVColorContainer].
*
* @param rgb The [RGBByteColorContainer] to convert.
* @return [HSVColorContainer]
*/
fun rgbToHsv(rgb: RGBByteColorContainer): HSVColorContainer = rgbToHsv(rgb.toFloatContainer())

/**
* Converts a [RGBFloatColorContainer] to a [HSVColorContainer].
*
* @param rgb The [RGBFloatColorContainer] to convert.
* @return [HSVColorContainer]
*/
fun rgbToHsv(rgb: RGBFloatColorContainer): HSVColorContainer {
val max = maxOf(rgb.red, rgb.green, rgb.blue)
val min = minOf(rgb.red, rgb.green, rgb.blue)
val d = max - min
val s = if (max == 0f) 0f else d / max
val h = when {
d == 0f -> 0f
max == rgb.red -> (rgb.green - rgb.blue) / d + (if (rgb.green < rgb.blue) 6 else 0)
max == rgb.green -> (rgb.blue - rgb.red) / d + 2
else -> (rgb.red - rgb.green) / d + 4
}
return HSVColorContainer(h, s, max)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.vitrivr.engine.core.model.color.hsv

/**
* A container for HSV colors.
*
* @author Ralph Gasser
* @version 1.0.0
*/
@JvmInline
value class HSVColorContainer private constructor(private val colors: FloatArray) {

constructor(hue: Float, saturation: Float, value: Float) : this(floatArrayOf(hue, saturation, value))
constructor(hue: Double, saturation: Double, value: Double) : this(floatArrayOf(hue.toFloat(), saturation.toFloat(), value.toFloat()))

/** Accessor for the hue component of the [HSVColorContainer]. */
val hue: Float
get() = this.colors[0]

/** Accessor for the saturation component of the [HSVColorContainer]. */
val saturation: Float
get() = this.colors[1]

/** Accessor for the value component of the [HSVColorContainer]. */
val value: Float
get() = this.colors[2]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.vitrivr.engine.core.model.color
package org.vitrivr.engine.core.model.color.rgb

import kotlin.math.max
import kotlin.math.min
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.vitrivr.engine.core.model.color
package org.vitrivr.engine.core.model.color.rgb

/**
* A container for RGB colors.
Expand Down Expand Up @@ -31,7 +31,9 @@ value class RGBByteColorContainer(private val rgb: Int) {
get() = (this.rgb and 0xFF).toUByte()

/**
* Converts this [RGBByteColorContainer] to an [RGBFloatColorContainer].
*
* @return [RGBFloatColorContainer] representation of this [RGBByteColorContainer].
*/
fun toFloatContainer(): RGBFloatColorContainer =
RGBFloatColorContainer(this.red.toFloat() / 255f, this.green.toFloat() / 255f, this.blue.toFloat() / 255f)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.vitrivr.engine.core.model.color
package org.vitrivr.engine.core.model.color.rgb

import org.vitrivr.engine.core.model.types.Value
import kotlin.math.max
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package org.vitrivr.engine.index.aggregators.image

import org.vitrivr.engine.core.context.Context
import org.vitrivr.engine.core.context.IndexContext
import org.vitrivr.engine.core.model.color.MutableRGBFloatColorContainer
import org.vitrivr.engine.core.model.color.RGBByteColorContainer
import org.vitrivr.engine.core.model.color.rgb.MutableRGBFloatColorContainer
import org.vitrivr.engine.core.model.color.rgb.RGBByteColorContainer
import org.vitrivr.engine.core.model.content.decorators.SourcedContent
import org.vitrivr.engine.core.model.content.decorators.TemporalContent
import org.vitrivr.engine.core.model.content.element.ContentElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package org.vitrivr.engine.index.aggregators.image

import org.vitrivr.engine.core.context.Context
import org.vitrivr.engine.core.context.IndexContext
import org.vitrivr.engine.core.model.color.MutableRGBFloatColorContainer
import org.vitrivr.engine.core.model.color.RGBByteColorContainer
import org.vitrivr.engine.core.model.color.rgb.MutableRGBFloatColorContainer
import org.vitrivr.engine.core.model.color.rgb.RGBByteColorContainer
import org.vitrivr.engine.core.model.content.element.ContentElement
import org.vitrivr.engine.core.model.content.element.ImageContent
import org.vitrivr.engine.core.model.retrievable.Ingested
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package org.vitrivr.engine.module.features.feature.huehistogram

import org.vitrivr.engine.core.context.IndexContext
import org.vitrivr.engine.core.context.QueryContext
import org.vitrivr.engine.core.model.color.ColorConverter
import org.vitrivr.engine.core.model.color.hsv.HSVColorContainer
import org.vitrivr.engine.core.model.color.rgb.RGBByteColorContainer
import org.vitrivr.engine.core.model.content.Content
import org.vitrivr.engine.core.model.content.element.ContentElement
import org.vitrivr.engine.core.model.content.element.ImageContent
import org.vitrivr.engine.core.model.descriptor.vector.FloatVectorDescriptor
import org.vitrivr.engine.core.model.metamodel.Analyser
import org.vitrivr.engine.core.model.metamodel.Schema
import org.vitrivr.engine.core.model.query.Query
import org.vitrivr.engine.core.model.query.proximity.ProximityQuery
import org.vitrivr.engine.core.model.retrievable.Retrievable
import org.vitrivr.engine.core.model.types.Value
import org.vitrivr.engine.core.operators.Operator
import org.vitrivr.engine.core.operators.ingest.Extractor
import org.vitrivr.engine.core.operators.retrieve.Retriever
import java.util.*
import kotlin.reflect.KClass

/**
* A hue histogram [Analyser] for [ImageContent] objects.
*
* Migrated from Cineast.
*
* @author Ralph Gasser
* @version 1.0.0
*/
class HueHistogram : Analyser<ImageContent, FloatVectorDescriptor> {

companion object {
private const val VECTOR_SIZE = 16
}

override val contentClasses: Set<KClass<out ContentElement<*>>> = setOf(ImageContent::class)
override val descriptorClass: KClass<FloatVectorDescriptor> = FloatVectorDescriptor::class

/**
* Generates a prototypical [FloatVectorDescriptor] for this [HueHistogram].
*
* @param field [Schema.Field] to create the prototype for.
* @return [FloatVectorDescriptor]
*/
override fun prototype(field: Schema.Field<*, *>): FloatVectorDescriptor = FloatVectorDescriptor(UUID.randomUUID(), UUID.randomUUID(), Value.FloatVector(VECTOR_SIZE))

/**
* Generates and returns a new [HueHistogramExtractor] instance for this [HueHistogram].
*
* @param name The name of the [HueHistogramExtractor].
* @param input The [Operator] that acts as input to the new [Extractor].
* @param context The [IndexContext] to use with the [Extractor].
*
* @return A new [Extractor] instance for this [Analyser]
* @throws [UnsupportedOperationException], if this [Analyser] does not support the creation of an [Extractor] instance.
*/
override fun newExtractor(name: String, input: Operator<Retrievable>, context: IndexContext) = HueHistogramExtractor(input, this, null)

/**
* Generates and returns a new [HueHistogramExtractor] instance for this [HueHistogram].
*
* @param field The [Schema.Field] to create an [Extractor] for.
* @param input The [Operator] that acts as input to the new [Extractor].
* @param context The [IndexContext] to use with the [Extractor].
*
* @return A new [Extractor] instance for this [Analyser]
* @throws [UnsupportedOperationException], if this [Analyser] does not support the creation of an [Extractor] instance.
*/
override fun newExtractor(field: Schema.Field<ImageContent, FloatVectorDescriptor>, input: Operator<Retrievable>, context: IndexContext) = HueHistogramExtractor(input, this, field)

/**
* Generates and returns a new [HueHistogramRetriever] instance for this [HueHistogram].
*
* @param field The [Schema.Field] to create an [Retriever] for.
* @param query The [Query] to use with the [Retriever].
* @param context The [QueryContext] to use with the [Retriever].
*
* @return A new [HueHistogramRetriever] instance for this [HueHistogram]
*/
override fun newRetrieverForQuery(field: Schema.Field<ImageContent, FloatVectorDescriptor>, query: Query, context: QueryContext): HueHistogramRetriever {
require(field.analyser == this) { "The field '${field.fieldName}' analyser does not correspond with this analyser. This is a programmer's error!" }
require(query is ProximityQuery<*> && query.value is Value.FloatVector) { "The query is not a ProximityQuery<Value.FloatVector>." }
@Suppress("UNCHECKED_CAST")
return HueHistogramRetriever(field, query as ProximityQuery<Value.FloatVector>, context)
}

/**
* Generates and returns a new [HueHistogramRetriever] instance for this [HueHistogram].
*
* Invoking this method involves converting the provided [FloatVectorDescriptor] into a [ProximityQuery] that can be used to retrieve similar [ImageContent] elements.
*
* @param field The [Schema.Field] to create an [Retriever] for.
* @param descriptors An array of [FloatVectorDescriptor] elements to use with the [Retriever]
* @param context The [QueryContext] to use with the [Retriever]
*/
override fun newRetrieverForDescriptors(field: Schema.Field<ImageContent, FloatVectorDescriptor>, descriptors: Collection<FloatVectorDescriptor>, context: QueryContext): HueHistogramRetriever {
require(field.analyser == this) { "The field '${field.fieldName}' analyser does not correspond with this analyser. This is a programmer's error!" }

/* Prepare query parameters. */
val k = context.getProperty(field.fieldName, "limit")?.toLongOrNull() ?: 1000L
val fetchVector = context.getProperty(field.fieldName, "returnDescriptor")?.toBooleanStrictOrNull() ?: false

/* Return retriever. */
return this.newRetrieverForQuery(field, ProximityQuery(value = descriptors.first().vector, k = k, fetchVector = fetchVector), context)
}

/**
* Generates and returns a new [HueHistogramRetriever] instance for this [HueHistogram].
*
* Invoking this method involves converting the provided [ImageContent] and the [QueryContext] into a [FloatVectorDescriptor]
* that can be used to retrieve similar [ImageContent] elements.
*
* @param field The [Schema.Field] to create an [Retriever] for.
* @param content An array of [Content] elements to use with the [Retriever]
* @param context The [QueryContext] to use with the [Retriever]
*/
override fun newRetrieverForContent(field: Schema.Field<ImageContent, FloatVectorDescriptor>, content: Collection<ImageContent>, context: QueryContext) = this.newRetrieverForDescriptors(field, content.map { this.analyse(it) }, context)

/**
* Performs the [HueHistogram] analysis on the provided [ImageContent] and returns a [FloatVectorDescriptor] that represents the result.
*
* @param content [ImageContent] to be analysed.
* @return [FloatVectorDescriptor] result of the analysis.
*/
fun analyse(content: ImageContent): FloatVectorDescriptor {
/* Generate histogram. */
val hist = FloatArray(VECTOR_SIZE)
val colors = content.content.getRGB(0, 0, content.content.width, content.content.height, null, 0, content.content.width)
for (color in colors) {
val container: HSVColorContainer = ColorConverter.rgbToHsv(RGBByteColorContainer(color))
if (container.saturation > 0.2f && container.value > 0.3f) {
val h: Float = container.hue * hist.size
val idx = h.toInt()
hist[idx] += h - idx
hist[(idx + 1) % hist.size] += idx + 1 - h
}
}

/* Normalize hist. */
val sum = hist.sum()
if (sum > 1f) {
for (i in hist.indices) {
hist[i] /= sum
}
}

return FloatVectorDescriptor(vector = Value.FloatVector(hist))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.vitrivr.engine.module.features.feature.huehistogram

import org.vitrivr.engine.core.features.AbstractExtractor
import org.vitrivr.engine.core.features.metadata.source.file.FileSourceMetadataExtractor
import org.vitrivr.engine.core.model.content.ContentType
import org.vitrivr.engine.core.model.content.element.ImageContent
import org.vitrivr.engine.core.model.descriptor.Descriptor
import org.vitrivr.engine.core.model.descriptor.vector.FloatVectorDescriptor
import org.vitrivr.engine.core.model.metamodel.Schema
import org.vitrivr.engine.core.model.retrievable.Retrievable
import org.vitrivr.engine.core.operators.Operator
import org.vitrivr.engine.core.operators.ingest.Extractor
import org.vitrivr.engine.core.source.file.FileSource
import org.vitrivr.engine.module.features.feature.ehd.EHD

/**
* An [Extractor] implementation for the [HueHistogram] analyser.
*
* @see [EHD]
*
* @author Ralph Gasser
* @version 1.0.0
*/
class HueHistogramExtractor(input: Operator<Retrievable>, analyser: HueHistogram, field: Schema.Field<ImageContent, FloatVectorDescriptor>?) : AbstractExtractor<ImageContent, FloatVectorDescriptor>(input, analyser, field) {
/**
* Internal method to check, if [Retrievable] matches this [Extractor] and should thus be processed.
*
* [FileSourceMetadataExtractor] implementation only works with [Retrievable] that contain a [FileSource].
*
* @param retrievable The [Retrievable] to check.
* @return True on match, false otherwise,
*/
override fun matches(retrievable: Retrievable): Boolean = retrievable.content.any { it.type == ContentType.BITMAP_IMAGE }

/**
* Internal method to perform extraction on [Retrievable].
*
* @param retrievable The [Retrievable] to process.
* @return List of resulting [Descriptor]s.
*/
override fun extract(retrievable: Retrievable): List<FloatVectorDescriptor> {
val content = retrievable.content.filterIsInstance<ImageContent>()
return content.map { (this.analyser as HueHistogram).analyse(it).copy(retrievableId = retrievable.id, field = this.field) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.vitrivr.engine.module.features.feature.huehistogram

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.flow
import org.vitrivr.engine.core.context.QueryContext
import org.vitrivr.engine.core.features.AbstractRetriever
import org.vitrivr.engine.core.math.correspondence.LinearCorrespondence
import org.vitrivr.engine.core.model.content.element.ImageContent
import org.vitrivr.engine.core.model.descriptor.vector.FloatVectorDescriptor
import org.vitrivr.engine.core.model.metamodel.Schema
import org.vitrivr.engine.core.model.query.proximity.ProximityQuery
import org.vitrivr.engine.core.model.retrievable.attributes.DistanceAttribute
import org.vitrivr.engine.core.model.types.Value
import org.vitrivr.engine.core.operators.retrieve.Retriever

/**
* [Retriever] implementation for the [HueHistogram] analyser.
*
* @see [HueHistogram]
*
* @author Ralph Gasser
* @version 1.0.0
*/
class HueHistogramRetriever(field: Schema.Field<ImageContent, FloatVectorDescriptor>, query: ProximityQuery<Value.FloatVector>, context: QueryContext) : AbstractRetriever<ImageContent, FloatVectorDescriptor>(field, query, context) {

companion object {
/** [LinearCorrespondence] for [HueHistogramRetriever]. Maximum distance is taken from Cineast implementation. */
private val CORRESPONDENCE = LinearCorrespondence(16f)
}

override fun toFlow(scope: CoroutineScope) = flow {
this@HueHistogramRetriever.reader.queryAndJoin(this@HueHistogramRetriever.query).forEach {
val distance = it.filteredAttribute<DistanceAttribute>()
if (distance != null) {
it.addAttribute(CORRESPONDENCE(distance))
} else {
this@HueHistogramRetriever.logger.warn { "No distance attribute found for descriptor ${it.id}." }
}
emit(it)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.vitrivr.engine.module.features.feature.ehd.EHD
org.vitrivr.engine.module.features.feature.huehistogram.HueHistogram
org.vitrivr.engine.module.features.feature.external.implementations.dino.DINO
org.vitrivr.engine.module.features.feature.external.implementations.clip.CLIP
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class ImageCaptioningApi(host: String, model: String, timeoutMs: Long, pollingIn
*/
override suspend fun pollBatchedJob(jobId: String): JobResult<List<Value.Text>> = try {
this.imageCaptioningApi.getBatchedJobResultsApiTasksImageCaptioningBatchedJobsJobGet(jobId).body().let { result ->
val values = result.result?.map { Value.Text(it.caption.trim() ?: "") }
val values = result.result?.map { Value.Text(it.caption.trim()) }
JobResult(result.status, values)
}
} catch (e: Throwable) {
Expand Down

0 comments on commit 2edf650

Please sign in to comment.