京セラ feelH” Treva カメラ
2022.08.12
YouTube でも紹介しています。画像をクリックすると再生できます。
今回は、京セラのデジタルカメラユニット「Treva」を使ってデジタルカメラを作ってみました。
■feel H"端末専用の小型デジタルカメラユニット 「Treva(トレバ)」
2000年9月22日,DDIポケットはデジカメを装着できるPHS「feel H"」を発表。
左側の写真は三洋電機のfeel H"端末「Leje」で、携帯の左下に移っているのが,右側の写真、京セラのデジタルカメラユニット「Treva」です。
同月28日にはDDIセルラーグループと日本移動通信(株)は、cdmeOne携帯電話向けのデジタルカメラ『PashaPa』(パシャパ)を発表しています。
今回は先人により仕様が解明されているTreva(トレバ)をメルカリで送料込み350円で購入しました。
携帯に外付けカメラ?とふと思って調べてみると、最初のカメラ付き携帯は2000年、J-phone(現在のソフトバンクモバイル)の SHARP「J-SH04」という機種だそうです。
この時期、カメラ付き携帯電話はまだ誕生期にあたり、外付けカメラのTrevaも画期的なものでした。
その後、カメラ付き携帯は2003年末には200万画素、2006年中旬には500万画素と進化を遂げていき、外付けカメラは市場から消えていきました。
■Treva(型番:HC-D01)の主な仕様
| 製品名 |
HC-D01 「Treva」 |
| 撮像素子 |
1/4型CMOSセンサー |
| 画像出力サイズ |
横96×縦72ピクセル |
| レンズ |
固定焦点(撮影距離範囲:30センチ~無限遠) |
| 露出・ホワイトバランス |
自動 |
| インタフェース |
2.5ミリ4ピンプラグ |
| 電源 |
端末から供給 |
| 本体サイズ |
約30(幅)×16(高さ)×32(厚さ)ミリ |
| 重量 |
約10グラム |
いまでは信じられないほど貧相な解像度です。でも当時は画期的だったのでしょう?
焦点が30cmからとありますが、5cmの距離でも焦点が合います。
なお、この解像度は左の写真のようにミニジャックを横にした場合の解像度です。
■カメラモジュールの作成
使いやすいようにカメラモジュールの形に作り替えます。
白いキャップを外すと、ちょっと特殊な4極2.5mmのミニジャックが現れます。
このミニジャックに対応しているソケットが見つかりませんので、直接配線を引き出すことにします。
ミニジャック両脇のフックをマイナスドライバーで押し込んで、蓋を開けます。

クロック信号がピンク色の線、データ信号が黄色の線 GNDが黒線、3.3Vが赤線です。
ニッパーを用いて、ミニジャックにハンダ付けされているこの4線を切断します。

4本の線の先端の剥がしてちょっとだけ導線を剝き出しにします。

次にユニバーサル基板を用意して、L字型ヘッダーピンをハンダ付けします。


先程の4線をユニバーサル基板の穴に通して、裏側からハンダ付けします。

Trevaを両面テープを使って基板に固定すれば、シリアルカメラモジュールの完成です。
基板に固定することで、細い線が断線する心配もありません。

ブレッドボードに挿すとこんな感じになります。
■開発環境・構成図

ノートパソコンから、TeraTermによりラズベリーパイにSSH接続して操作します。
3.3V 動作で、マイコンからクロックを供給し、Treva からのシリアルデータを読み込みます。
■Adafruit QT Py ESP32-S2 WiFi Dev Board with STEMMA QT
QT Py ESP32 -S2にはシングルコアの240MHzチップが搭載されているため、デュアルコアのESP32ほど高速ではありませんが、
4MBのフラッシュと2MBのPSRAMを実装しているため、データ解析などに必要なメモリー空間を確保することができます。
Adafruit QT Py ESP32-S2
■Adafruit 1.14" 240x135 Color TFT Display + MicroSD Card Breakout - ST7789
The TFT driver (ST7789) is very similar to the popular ST7735, and our Arduino library supports it well.
There was a little space so Adafruit placed a microSD card holder so you can easily load full color bitmaps from a FAT16/FAT32 formatted microSD card. (GREENTAB)
Adafruit の MicroSD Card スロット付ディスプレイですが、SDカードから画像を読み込んで表示するには問題ないのですが、
SDカードに書き込みを行おうとすると、表示が固まってしまいます。
There was a little space so Adafruit placed a microSD card holder so you can easily load full color bitmaps from a FAT16/FAT32 formatted microSD card.
loadとはありますが、saveとは記載がないので、表示中の書き込みは苦手?なのかもしれません。
そこで、別途SDカードモジュールを用意します。
■microSDカードスロット レベルシフタ付きブレークアウト基板キット
ESP32は、3.3V駆動なのでレベルシフタ付きモジュールは必要ないのですが、手元にあったのでこれを使用します。
microSDカードスロットに、レベルシフター(74HC4050)とボルテージレギュレータ(3.3V)を付加して、Arduino等の5V系回路へ直結。
販売:秋月電子通商
Arduino とは、5V,GND,SCL,MISO,MOSI,SS(計6本)を接続し、
ジャンパJ1をショート(はんだを盛って接続)して使用します。
SDカードの電源On/Off制御を行う場合は、ジャンパJ1をショートせず、[2]ピンを空いているデジタルピン(出力)に接続し、
スケッチ上より制御します(HIGHで電源ON)。
SDカード検出スイッチを使用する場合は、[9]ピンを空いているデジタルピン(入力)に接続し、
スケッチ上で読み取ります(検出時にLOW)。
■配線
| RASPBERRY PI | - | QT Py | - | |
| USB | - | USB | | |
| ST7789 | | | | micro SD |
| TFTCS | - | [0]A0 | | |
| DC | - | [1]A1 | | |
| RST | - | [2]A2 | | |
| | | [3]A3 | | [8]SDカード チップセレクト(CS)) |
| LIT | - | [6]A6(TX) | | |
| SCK | - | [8]SCK | | [5]SDカード クロック(CLK) |
| | | [9]MI | | [6]SDカード データ(DAT) |
| MOSI | - | [10]MO | | [7]SDカード コマンド(CMD) |
| 3V | - | 3.3V | - | |
| GND | - | GND | - | [4]GND |
| | | 5V | - | [1]+5V電源入力(+4.0V~+6.5V) |
| | - | | - | TREVA |
| | | [4]SDA | - | SDA |
| | | [5]SCL | | SCL |
| | - | 3.3V | - | 3.3V |
| | - | GND | - | GND |
| SWITCH | | | | |
SWITCH (pull up) | - | [7]A7(RX) | | |
| - | GND | | |
※I2C用ピン(SDA,SCL)を使っていますが、I2C通信ではありません
■Trevaカメラ外観

ブレッドボード・オブジェとしてはかなりアートな仕上がりかもしれません。

撮影者側には被写体確認用のディスプレイとシャッタースイッチがあります。
■ビルド環境
ソースコードのビルドには、PlatformIOを使用しています。
Arduino開発環境構築 PlatformIO
■ソースコード
プログラムは、verusさんのソースコードをベースに加筆しています。
Ref.TrevaなるカメラをArduinoにつなげてみる.
$ vi src/treva.ino
#include <SPI.h>
#include <SD.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#define SPI_TFT_CS A0
#define SPI_DC A1
#define SPI_MISO MISO
#define SPI_MOSI MOSI
#define SPI_SCK SCK
#define SPI_RST A2
#define SPI_SD_SS A3
#define SPI_BLK A6
#define SWITCH_PIN A7
Adafruit_ST7789 tft = Adafruit_ST7789(SPI_TFT_CS, SPI_DC, SPI_MOSI, SPI_SCK, SPI_RST);
#define CLK_PIN SCL
#define DAT_PIN SDA
SPIClass SDSPI(HSPI);
typedef struct {
uint8_t Signature[2]; // 'BM'
uint8_t FileSize[4];
uint8_t reserved1[4]; // unused (=0)
uint8_t DataOffset[4];
uint8_t Size[4]; // Size of InfoHeader =40
uint8_t Width[4];
uint8_t Height[4];
uint8_t Planes[2]; // always 1
uint8_t BitPerPixel[2];
uint8_t Compression[4];
uint8_t ImageSize[4];
uint8_t XpixelsPerM[4];
uint8_t YpixelsPerM[4];
uint8_t ColorsUsed[4];
uint8_t ImportantColors[4];
} BMP_FORMAT;
typedef struct {
uint32_t Width;
uint32_t Height;
uint16_t BitPerPixel;
uint32_t FileSize;
uint32_t DataOffset;
uint32_t ImageSize;
uint32_t ColorsUsed;
} IMAGE_INFO;
IMAGE_INFO *img;
typedef struct {
uint8_t R;
uint8_t G;
uint8_t B;
} PIXEL_INFO;
PIXEL_INFO *RGBs = NULL;
PIXEL_INFO *pixels = NULL;
uint16_t swap16(uint8_t *pt) {
uint16_t ret;
ret = *pt;
ret |= (*(pt+1)<<8)&0xff00;
return ret;
}
void short2byte(uint8_t *pt, uint16_t val) {
*pt = (uint8_t)val &0x00ff;
*(pt+1) = (uint8_t)((val>>8)&0x00ff);
}
uint32_t swap32(uint8_t *pt) {
uint32_t ret;
ret = *pt;
ret |= (*(pt+1)<< 8)&0x0000ff00;
ret |= (*(pt+2)<<16)&0x00ff0000;
ret |= (*(pt+3)<<24)&0xff000000;
return ret;
}
void long2byte(uint8_t *pt, uint32_t val) {
*pt = (uint8_t)val &0x000000ff;
*(pt+1) = (uint8_t)((val>> 8)&0x000000ff);
*(pt+2) = (uint8_t)((val>>16)&0x000000ff);
*(pt+3) = (uint8_t)((val>>24)&0x000000ff);
}
int readbit() {
int a;
digitalWrite(CLK_PIN, HIGH);
a = digitalRead(DAT_PIN);
digitalWrite(CLK_PIN, LOW);
if(a){
return 1;
} else {
return 0;
}
}
int saveBmpFile(const char *fileName) {
BMP_FORMAT *Bmp;
PIXEL_INFO *imgpt = RGBs;
char fname[20];
static int fno = 0;
fno++;
sprintf(fname,"%s%d.bmp",fileName,fno);
SDSPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, SPI_SD_SS);
pinMode(SPI_SD_SS, OUTPUT);
if (!SD.begin(SPI_SD_SS, SDSPI, 20000000)) return 0;
if (SD.exists(fname)) SD.remove(fname);
File fileSD = SD.open(fname, "wb");
if (!fileSD) return 0;
Bmp = (BMP_FORMAT *)malloc(sizeof(BMP_FORMAT));
memset(Bmp, 0x00, sizeof(BMP_FORMAT));
img->ImageSize = img->Width * img->Height * 3;
memcpy(&Bmp->Signature,(uint8_t*)"BM",2);
long2byte(Bmp->FileSize, sizeof(BMP_FORMAT) + img->ImageSize);
long2byte(Bmp->DataOffset, sizeof(BMP_FORMAT));
long2byte(Bmp->Size, 40);
long2byte(Bmp->Width, img->Width);
long2byte(Bmp->Height, img->Height);
short2byte(Bmp->Planes, 1);
short2byte(Bmp->BitPerPixel, 24);
long2byte(Bmp->ImageSize, img->ImageSize);
long2byte(Bmp->ColorsUsed, 0);
fileSD.write((uint8_t*)Bmp, sizeof(BMP_FORMAT));
int32_t x, y;
int32_t pos = 0;
uint8_t mod = (img->Width % 4) * 3; // RGB情報は画像横一列が4byteの倍数でなければならないための補正
for(y=0; y < img->Height; y++){
for(x=0; x < img->Width; x++,pos++,imgpt++){
fileSD.write(&imgpt->B, 1);
fileSD.write(&imgpt->G, 1);
fileSD.write(&imgpt->R, 1);
}
if (mod>0) fileSD.write(0x00,mod);
}
fileSD.close();
SD.end();
SDSPI.end();
free(Bmp);
return 1;
}
void capture() {
long magic = 0,i,k;
int v = 0, u = 0, y = 0;
int r = 0, g = 0, b = 0;
unsigned char d;
PIXEL_INFO *pt = pixels;
while((magic & 0xffffff) != 0xaa55ff) {
magic <<= 1;
if(readbit()) magic |= 0x01;
}
for(i=0; i<29*8; i++) readbit();
for( k=0; k < img->Width*img->Height*2; k++) {
d = 0;
for (i=0; i<8; i++) {
d <<= 1;
if(readbit()) d |= 0x1;
}
if((k & 0b11) == 0b00) { // 1st Byte
v = d - 128;
} else if((k & 0b11) == 0b10) { // 3rd Byte
u = d - 128;
} else if((k & 0b11) == 0b01 || (k & 0b11) == 0b11) { // Y0 or Y1
y = d;
r = u + y;
g = 0.98 * y - 0.53 * u - 0.19 * v;
b = v + y;
if(r > 255) r = 255;
if(r < 0) r = 0;
if(g > 255) g = 255;
if(g < 0) g = 0;
if(b > 255) b = 255;
if(b < 0) b = 0;
pt->R = r;
pt->G = g;
pt->B = b;
pt++;
}
}
pt = pixels;
for( int y=0; y < img->Height; y++) {
long pos = img->Width * (y + 1) -1;
for( int x=0; x < img->Width; x++, pos--, pt++) {
RGBs[pos].R = pt->R;
RGBs[pos].G = pt->G;
RGBs[pos].B = pt->B;
}
}
}
void setup() {
pinMode(CLK_PIN, OUTPUT);
pinMode(DAT_PIN, INPUT);
pinMode(SWITCH_PIN, INPUT_PULLUP);
img = (IMAGE_INFO *)malloc(sizeof(IMAGE_INFO));
img->Width = 96;
img->Height = 72;
RGBs = (PIXEL_INFO *)malloc(img->Width * img->Height * sizeof(PIXEL_INFO));
pixels = (PIXEL_INFO *)malloc(img->Width * img->Height * sizeof(PIXEL_INFO));
ledcSetup(0,12800,8);
ledcAttachPin(SPI_BLK,0);
ledcWrite(0,64);
tft.init(135,240);
tft.setRotation(0);
tft.fillScreen(ST77XX_BLACK);
}
void loop() {
int x, y;
long cnt;
capture();
for(y=0, cnt=0; y < img->Height; y++) {
for(x=img->Width - 1; x >= 0; x--, cnt++) {
tft.drawPixel(x+19,y+84,tft.color565(RGBs[cnt].R,RGBs[cnt].G,RGBs[cnt].B));
}
}
if (!digitalRead(SWITCH_PIN)) {
saveBmpFile("/treva");
tft.init(135,240);
tft.setRotation(0);
tft.fillScreen(ST77XX_BLACK);
}
}
●コード解説
ビットデータの取得
int readbit() {
int a;
digitalWrite(CLK_PIN, HIGH);
a = digitalRead(DAT_PIN);
digitalWrite(CLK_PIN, LOW);
if(a){
return 1;
} else {
return 0;
}
}
SCLの立ち上がり時にSDAのデータの読み込みが行われます。
ヘッダー情報の取得と読み飛ばし
while((magic & 0xffffff) != 0xaa55ff) {
magic <<= 1;
if(readbit()) magic |= 0x01;
}
for(i=0; i<29*8; i++) readbit();
Treva から送信されるデータの先頭は、0xAA 55 FF から始まる 32 byte のヘッダになります。
画像データの読み込み
for( k=0; k < img->Width*img->Height*2; k++) {
d = 0;
for (i=0; i<8; i++) {
d <<= 1;
if(readbit()) d |= 0x1;
}
if((k & 0b11) == 0b00) { // 1st Byte
v = d - 128;
} else if((k & 0b11) == 0b10) { // 3rd Byte
u = d - 128;
} else if((k & 0b11) == 0b01 || (k & 0b11) == 0b11) { // Y0 or Y1
y = d;
画像データの色空間は YUV で、UYVYフォーマット(YUV422)が採用されています。
Y:輝度信号 /
U (B-Y):色差信号(Cb) /
V (R-Y):色差信号(Cr)
UYVYフォーマットでは、1マクロピクセル(u_int32)の中に画像の2ピクセルが入ります。
この形式(YUV422)では、輝度信号(白黒)が色差信号(カラー)の二倍の解像度があります。
したがって、色解像度は 48x72画素分、輝度解像度は 96x72画素分になり、 これを考慮してYUV-RGB変換を行います。
Treva では、UYVY ではなく、8bit単位でV→Y→U→Y の順で送信されているようです。
※Trevaに電源を入れて最初の1枚目はダミー画像(黒画像)が送信されます。
| MIN | MAX | AVG |
| Y | 16 | 250 | 126 |
| U | 0 | 249 | 119 |
| V | 95 | 249 | 124 |
|
ちなみに、カメラを適当な方向に向けて撮影してみたところ、写真1枚のYUV値は下記のような分布になりました。
|
※YUVとは
「人間の目は明るさの変化には敏感だが, 色の変化には鈍感である」 というわけで,色度を抑え、輝度により広い帯域やビット数を
割くことにより、少ない損失で効率の良い伝送や圧縮を実現するフォーマット.
r = u + y;
g = 0.98 * y - 0.53 * u - 0.19 * v;
b = v + y;
YUV→RGB変換の部分です。色々な変換式がありますので、試してみてください。
■BMPファイルとして保存
この処理では撮影画像をBMP形式で保存しています。詳細は下記をご覧ください。
減色処理 雑談
■撮影

プログラムを起動するとカメラからの映像が流れます。
被写体の構図が決まったところでシャッタースイッチを押すと映し出されている画像がSDカードに保存されます。

Trevaの96×72ピクセルの解像度を考えると、風景撮影には向きません。当時はきっと友達の写真を撮って楽しんでいたのでしょう。
ということで、ワンピースのCARIFAさんを撮影してみました。
う~む、どうなんでしょう、雰囲気だけは伝わるかもしれません。
■参考文献
・TrevaなるカメラをArduinoにつなげてみる.
・テニスボールを認識する移動ロボット
・TrevaをZaurusに接続しよう
・YUVフォーマット及び YUV<->RGB変換
|
Raspberry Pi(ラズベリー パイ)は、ARMプロセッサを搭載したシングルボードコンピュータ。イギリスのラズベリーパイ財団によって開発されている。
たいていのことは100日あれば、うまくいく。長田英知著
「時間がなくて、なかなか自分のやりたいことができない」
「一念発起して何かを始めても、いつも三日坊主で終わってしまう」
「色んなことを先延ばしにしたまま、時間だけが過ぎていく」
そこで本書では、そんな著者が独自に開発した、
まったく新しい目標達成メソッド「100日デザイン」について、
その知識と技術を、余すところなくご紹介します。
まんがで納得ナポレオン・ヒル 思考は現実化する
OLとして雑務をこなす日々に飽き足らず、科学者だった父が残した薬品を商品化すべく、起業を決意した内山麻由(27)。彼女はセミナーで知り合った謎の女性からサポートを得ながら、彼女と二人三脚でナポレオン・ヒルの成功哲学を実践し、さまざまな問題を乗り越えていく。
ヒル博士の<ゴールデンルール>に従い、仕事に、恋に全力疾走する彼女の、成功への物語。
今日は人生最悪で最高の日 1秒で世界を変えるたったひとつの方法 ひすいこたろう著
偉人の伝記を読むと、最悪な日は、不幸な日ではなく、新しい自分が始まる日であることがわかります。最悪な出来事は、自分の人生が、想像を超えて面白くなる兆しなのです。偉人伝を読むことで、このときの不幸があったおかげで、未来にこういう幸せがくるのかと、人生を俯瞰する視線が立ち上がるのです。
ご飯は私を裏切らない heisoku著
辛い現実から目を背けて食べるご飯は、いつも美味しく幸せを届けてくれる。
29歳、中卒、恋人いない歴イコール年齢。バイト以外の職歴もなく、短期バイトを転々とする日々。ぐるぐると思索に耽るけど、ご飯を食べると幸せになれる。奇才の新鋭・heisokuが贈るリアル労働グルメ物語!
【最新版Gemini 3に対応!】できるGemini (できるシリーズ)
Geminiを「最強の知的生産パートナー」として使いこなすための、実践的なノウハウを凝縮した一冊です。
基本的な操作方法から、具体的なビジネスシーンでの活用、日々の業務を自動化するGoogle Workspaceとの連携、さらには自分だけのオリジナルAIを作成する方法まで余すところなく解説します。
Rustプログラミング完全ガイド 他言語との比較で違いが分かる!
Rustの各手法や考え方を幅広く解説!
500以上のサンプルを掲載。実行結果も確認。
全24章の包括的なチュートリアル。
ポチらせる文章術
販売サイト・ネット広告・メルマガ・ブログ・ホームページ・SNS…
全WEB媒体で効果バツグン!
カリスマコピーライターが教える「見てもらう」「買ってもらう」「共感してもらう」すべてに効くネット文章術
小型で便利な Type-C アダプター USB C オス - USB3.1 オスアダプター
Type-C端子のマイコンボードをこのアダプタを介して直接Raspberry Piに挿すことができます。ケーブルなしで便利なツールです。
Divoom Ditoo Pro ワイヤレススピーカー
15W高音質重低音/青軸キーボード/Bluetooth5.3/ピクセルアート 専用アプリ/USB接続/microSDカード
電源供給USBケーブル スリム 【5本セット】
USB電源ケーブル 5V DC電源供給ケーブル スリム 【5本セット】 電源供給 バッテリー 修理 自作 DIY 電子工作 (100cm)
|